单点登录及实现方案原理
单点登录概念:
单点登录英文全称Single Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。例如,网页登录了淘宝账号,天猫,钉钉等阿里系应用都不用再二次登录了。 SSO核心意义就一句话:一处登录,处处登录;一处注销,处处注销。
一、基于cookie实现单点登录
这是最简单的单点登录实现方式,是使用cookie作为媒介,存放用户凭证。 用户登录父应用之后,应用返回一个加密的cookie,当用户访问子应用的时候,携带上这个cookie,授权应用解密cookie并进行校验,校验通过则登录当前用户。
缺点:
Cookie不安全不能跨域实现免登
对于第一个问题,通过加密Cookie可以保证安全性,当然这是在源代码不泄露的前提下。如果Cookie 的加密算法泄露,攻击者通过伪造Cookie则可以伪造特定用户身份,这是很危险的;对于第二个问题,不能跨域实现免登更是硬伤。因此,有了基于Session的单点登录。
cookie实现单点登录流程分析
需要做ip和域名映射:
#主系统
127.0.0.1 www.sso.com
#登录系统
127.0.0.1 login.sso.com
#vip子系统
127.0.0.1 vip.sso.com
#购物车子系统
127.0.0.1 cart.sso.com
用户首次登录,携带请求页url,跳转登录系统控制层接口
@Controller
@RequestMapping("view")
public class ViewController {
@Autowired
RestTemplate restTemplate;
//携带token跨服务请求登录服务接口获取已经登陆用户信息--若解决session共享则可以省略该步骤
private final String LOGIN_INFO_ADDRESS="http://login.sso.com:9001/login/info?token=";
@GetMapping("/index")
public String toIndex(@CookieValue(required = false,value = "TOKEN")Cookie cookie, HttpSession session){
if(null!=cookie){
String token = cookie.getValue();
if(StringUtils.isNotEmpty(token)){
Map result = restTemplate.getForObject(LOGIN_INFO_ADDRESS + token, Map.class);
session.setAttribute("loginUser",result);
}
}else {
session.removeAttribute("loginUser");
}
return "index";
}
首页
这里是首页展示页面
已登录
判断cookie为空,直接跳转登录界面
/**
* 页面跳转逻辑
*/
@Controller
@RequestMapping("view")
public class ViewController {
/**
* 跳转到登录页面
*
* @return
*/
@GetMapping("/login")
public String toLogin(@RequestParam(required = false, defaultValue = "") String target, HttpSession session,
@CookieValue(required = false, value = "TOKEN") Cookie cookie) {
if (StringUtils.isEmpty(target)) {
//未携带url设置默认页面
target = "http://www.sso.com:9000";
}
if (cookie != null) {
//如果是已经登录的用户再次访问登录系统时,就重定向到回页面
String token = cookie.getValue();
User user = LoginCacheUtil.loginUser.get(token);
if (user != null) {
return "redirect:" + target;
}
}
//未登录,跳转登录controller
session.setAttribute("target", target);
return "login";
}
}
登录界面
欢迎来到登录页面
登录页面表单提交,登录控制层接收用户信息并校验数据是否正确,用户存在,则将该用户信息保存(redis,或Map),创建cookie,将新建token存储在cookie中,并响应到HttpServletResponse中供其他系统访问,登录成功跳回请求系统控制层接口。
判断是否登录,已登录判断用户存在session,跳转会请求页面
@Controller
@RequestMapping("view")
public class ViewController {
/**
* 跳转到登录页面
*
* @return
*/
@GetMapping("/login")
public String toLogin(@RequestParam(required = false, defaultValue = "") String target, HttpSession session,
@CookieValue(required = false, value = "TOKEN") Cookie cookie) {
if (StringUtils.isEmpty(target)) {
//未携带url设置默认页面
target = "http://www.sso.com:9000";
}
if (cookie != null) {
//如果是已经登录的用户再次访问登录系统时,就重定向到回页面
String token = cookie.getValue();
User user = LoginCacheUtil.loginUser.get(token);
if (user != null) {
return "redirect:" + target;
}
}
//未登录,跳转登录controller
session.setAttribute("target", target);
return "login";
}
}
未登录请求登录接口
@Controller
@RequestMapping("/login")
public class LoginController {
// 模拟数据库用户
private static Set
static {
dbUser = new HashSet<>();
dbUser.add(new User(0, "zhangsan", "1234"));
dbUser.add(new User(1, "lisi", "1234"));
dbUser.add(new User(2, "中国魂", "123456"));
}
@PostMapping
public String doLogin(User user, HttpSession session, HttpServletResponse response) {
String target = (String) session.getAttribute("target");
// 模拟从数据库中通过登录的用户名和密码查找用户,
Optional
dbUser.getPassword().equals(user.getPassword())).findFirst();
// 判断用户是否登录d
if (first.isPresent()) {
// 保存用户登录信息
String token = UUID.randomUUID().toString();
LoginCacheUtil.loginUser.put(token, first.get());
session.removeAttribute("msg");
//设置cookie
Cookie cookie = new Cookie("TOKEN", token);
//cookie在子系统间访问域要相同
//cookie不能跨域
cookie.setDomain("sso.com");
/*
cookie过期时间设置方式:
cookie.setMaxAge(0);//不记录cookie
cookie.setMaxAge(-1);//会话级cookie,关闭浏览器失效
cookie.setMaxAge(60*60);//过期时间为1小时
*/
cookie.setMaxAge(-1);
cookie.setPath("/");
//通过HttpServletResponse将cookie响应到子系统
response.addCookie(cookie);
} else {
//登录失败
session.setAttribute("msg", "用户名或密码错误");
return "login";
}
// 重定向到target地址
return "redirect:" + target;
}
(用户回显)判断cookie不为空,从cookie中获取token,用token从redis中或者跨系统从Map中获取用户信息,并存入session,跳转请求页面显示登录用户。
子系统点击登录,在登录系统中判断cookie是否存在,存在则获取token,根据token获取登录用户信息,如果用户存在,则直接跳回请求的子系统页面。
子系统页面
这里是购物车展示页面
已登录
登录逻辑
@Controller
@RequestMapping("view")
public class ViewController {
@Autowired
RestTemplate restTemplate;
//携带token跨服务请求登录服务接口获取已经登陆用户信息--若解决session共享则可以省略该步骤
private final String LOGIN_INFO_ADDRESS="http://login.sso.com:9001/login/info?token=";
@GetMapping("/index")
public String toIndex(@CookieValue(required = false,value = "TOKEN")Cookie cookie, HttpSession session){
if(null!=cookie){
String token = cookie.getValue();
if(StringUtils.isNotEmpty(token)){
Map result = restTemplate.getForObject(LOGIN_INFO_ADDRESS + token, Map.class);
session.setAttribute("loginUser",result);
}
}else {
session.removeAttribute("loginUser");
}
return "index";
}
跨服务交互接口,从map中获取用户信息
/**
* 通过token获取用户登录信息
* 跨系统交互
*/
@GetMapping("/info")
@ResponseBody
public ResponseEntity
if (StringUtils.isNotEmpty(token)) {
User user = LoginCacheUtil.loginUser.get(token);
return ResponseEntity.ok(user);
}
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
public class LoginCacheUtil {
public static HashMap
}
退出单点登录:设置cookie过期时间为0,就ok。
//退出
@GetMapping("/exit")
public String toExit(HttpServletRequest request, HttpSession session, HttpServletResponse response) {
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if ("TOKEN".equals(cookie.getName())) {
cookie.setDomain("sso.com");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
}
return "redirect:/view/index";
}
pom文件
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">