不说官话, 只说白话, 越白越好
统一名词
- client1.com 第二个客户端
- client2.com 第一个客户端
- sso.com: 登录服务器域名
- sso-token: sso.com域名下携带的token, 也就是说在sso.com下的服务器登录, sso.com域名下就会生成一个key为sso-token的cookie, 这个生成的行为, 皆由后端处理
- 实现目的: 在client1或者client2其中之一登录了, 访问对方都可以直接访问, 实现一处登录, 处处访问
流程
1, 用户访问client1.com进行登录, 或者是资源client1.com/res,
访问资源肯定会被拦截到登录页面去 ,本示例以直接访问资源展开
浏览器访问:
client1.com/res
2, client1后端肯定会做登录校验, client1Server后端处理
- 校验用户是否登录, session里是否已经有了
- 参数中是否有token令牌
用户登录了, 就放行, 用户未登录, 重定向到sso.com的登录页面
public String example( HttpSession session,@RequestParam(value = "token",required = false) String token){
if(!StringUtils.isEmpty(token)){
//TODO 根据令牌, 发送http请求给ssoServer, 换取accessToken
//比如: "http://sso.com/xx?token=" + token
}
//看看session里是否有登录用户
Object loginUser = session.getAttribute("loginUser");
if(loginUser == null){
//跳转到登录服务器, 并且携带重定向页面
//ssoServerUrl sso.com的登录页面
//redirect_url: 登录成功之后, ssoServer需要回调你的页面
return "redirect:"+ssoServerUrl+"?redirect_url="+redirect_url;
}else{
//TODO 用户已经登录, 去数据查询数据, 然后返回
}
}
3, 用户被重定向到了, sso.com的登录请求(ssoServerUrl) 不是直接访问了ssoServer的前端登录页面哈 ssoServer处理
@GetMapping("/login.html")
public String loginPage(
@RequestParam("redirect_url") String url,
Model model,
@CookieValue(value = "sso_token",required = false) String ssoToken
) {
if(!StringUtils.isEmpty(ssoToken)){
//sso.com可以获取到cookie, 说明这个用户之前在别的地方(client1)登录过,
return "redirect:"+url+"?token="+ssoToken;
}
model.addAttribute("url",url);
return "login";
}
可以看到, 需要注意的是:
@CookieValue ssoToken 跳转到登录页面之前, 都会去先去当前域名下找一个叫sso_token的cookie, 如果这个值不是空的, 就会跳转到你重定向的页面, 就回到了第2步的位置, 这个也是实现sso的关键点
4, ssoServer验证用户没登录, 给用户重定向到sso.com的前端登录html页面 ssoServer
用户在这里输入账号密码, ssoServer进行登录
@PostMapping("/doLogin")
public String login(String username, String pwd, String url, HttpServletResponse response) {
//url: 登录成功跳转, 跳回到之前访问的页面
//TODO 实现你需要的登录逻辑, 验证账号密码, 防止csrf之类的
// 重点: 这里有一个uid, 其实就是你这个用户信息的一个标识, 下次不管是谁来访问, 可以根据这个uid, 找到用户信息
//但是这个uid并不代表就是用户信息, 是根据他去查询用户信息,
//比如这里可以将uid对应的用户信息, 放在redis里, 这样其他微服务也可以直接从redis里获取
String uid = UUID.randomUUID().toString().replaceAll("-", "");
redisTemplate.opsForValue().set(uid,username);
//TODO 重点: 给当前服务器(ssoServer), 当前域名(sso.com),留一个cookie, 记录用户已经登录了
//方便以后浏览器, 再访问sso.com的时候, 就会携带着这个cookie
Cookie cookie = new Cookie("sso_token",uid);
response.addCookie(cookie);
//然后携带令牌token, 重定向到client1.com去,注意这里没有用户信息, 只是一个登录的令牌
return "redirect:"+url+"?token="+uid;
}
5, 用户登录成功, 跳转到之前第一次访问的页面, client1.com/res client1Server
又跳到了第2步的代码里, 进行session验证, token验证, 用户是否已经登录, 成功就显示页面
接下来的步骤, 对用户无感,只是换了一个client2.com 但是角度从clientServer1变成了clientServer2
6, 用户浏览器访问client2.com/res
client2.com/res
7, client2Server接收到请求, 进行处理, 和第2步大体一样, client2Server
public String example( HttpSession session,@RequestParam(value = "token",required = false) String token){
if(!StringUtils.isEmpty(token)){
//TODO 根据令牌, 发送http请求给ssoServer, 换取accessToken
//比如: "http://sso.com/xx?token=" + token
}
//看看session里是否有登录用户
Object loginUser = session.getAttribute("loginUser");
if(loginUser == null){
//跳转到登录服务器, 并且携带重定向页面
//ssoServerUrl sso.com的登录页面
//redirect_url: 登录成功之后, ssoServer需要回调你的页面
return "redirect:"+ssoServerUrl+"?redirect_url="+redirect_url;
}else{
//TODO 用户已经登录, 去数据查询数据, 然后返回
}
}
注意不一样的点是在于:
- 用户登录了, 而且ssoServer里给sso.com域名下写了一个key为sso-token的cookie
- 如上, 这时候, client2Server的example方法是可以接收到token的值的, 因此他可以直接使用这个token获取到用户信息
8, 重定向到client2.com/res页面, client2Server
返回/res的资源请求给用户, 和第5步一样
这样子, 单点登录是完成了,重要的点就是
- ssoServer登录后, 得自己记录一个cookie, 相当于一个标识, 记录这个用户在这个浏览器上登录过, 他下次访问client2 的时候, 可以直接获取到这个令牌, 然后登录
- client登录以后, 在session中也得存着这个标识, 因为他需要用这个标识去ssoServer获取用户信息
当然还是有很多安全考虑的, 要做的事情还很多, 比如令牌只能使用一次了, 加密了之类的, 反正大概就是这个样子了!