一文搞懂单点登录 1 单点登录

  1. 单点登录

单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

对于相同父域名下的单点登录比较简单,只需要将cookie的作用域放大到父域名即可。

1
2
3
4
5
6
7
java复制代码@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("ylogin.com");
cookieSerializer.setCookieName("YLOGINESSION");
return cookieSerializer;
}

本文主要分享一下不同应用服务器之间(即不同域名)的单点登录流程。

  1. 单点登录流程

单点登录流程图如下

单点登录.png

  1. 假设现在第一次访问Client1的受保护的资源,由于我们没有登录,则需要跳转到登录服务器进行登录,但是登录之后应该跳到哪里呢?很显然,需要跳回到我们想要访问的页面,所以在重定向到登录服务器时带上回调地址redirectURL。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@GetMapping("/abc")
public String abc(HttpServletRequest request,HttpSession session, @RequestParam(value = "token",required = false) String token) throws Exception {
if (!StringUtils.isEmpty(token)){
Map<String,String> map = new HashMap<>();
map.put("token",token);
HttpResponse response = HttpUtils.doGet("http://auth.ylogin.com", "/loginUserInfo", "GET", new HashMap<String, String>(), map);
String s = EntityUtils.toString(response.getEntity());
if (!StringUtils.isEmpty(s)){
UserResponseVo userResponseVo = JSON.parseObject(s, new TypeReference<UserResponseVo>() {
});
session.setAttribute(AuthServerConstant.LOGIN_USER,userResponseVo);
localSession.put(token,session);
sessionTokenMapping.put(session.getId(),token);
}
}
UserResponseVo attribute = (UserResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null){
return "abc";
} else {
// 由于域名不同,不能实现session共享,无法在登录页面展示msg
session.setAttribute("msg","请先进行登录");
// 带上回调地址
return "redirect:http://auth.ylogin.com/login.html?redirectURL=http://ylogin.client1.com"+request.getServletPath();
}
}
  1. 浏览器展示登录页
  2. 用户输入账号密码进行登录,并在隐藏域提交回调地址
  3. 登录服务器查询数据库,验证账号及密码。账号密码正确,则生成一个令牌sso_token,保存到cookie中(该cookie只存在于登录服务器),并将登录用户信息以sso_token为key,保存到redis中(剧透,顺便保存回调地址到redis)。然后携带上令牌重定向到回调地址(即登录前页面)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码@PostMapping("/login")
public String login(UserLoginTo to, RedirectAttributes redirectAttributes, HttpServletResponse response) {
//远程登陆
R login = userFeignService.login(to);
if (login.getCode() == 0) {
UserResponseVo data = login.getData(new TypeReference<UserResponseVo>() {
});
log.info("登录成功!用户信息"+data.toString());
// 保存用户信息到redis(key->value:sso_token->登录用户信息)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(token, JSON.toJSONString(data),2, TimeUnit.MINUTES);
// 添加登录地址
addLoginUrl(to.getRedirectURL());
// 保存令牌到cookie
Cookie cookie = new Cookie("sso_token", token);
response.addCookie(cookie);
// 携带令牌重定向到回调地址
return "redirect:"+to.getRedirectURL()+"?token="+token;
} else {
Map<String, String> errors = new HashMap<>();
errors.put("msg", login.get("msg", new TypeReference<String>() {
}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.ylogin.com/login.html?redirectURL="+to.getRedirectURL();
}
}
  1. 应用服务器1拿到token,需要向验证服务器发起请求(也可以直接到redis中查是否存在这个key),验证是否存在该token。目的是为了防止伪造令牌。验证通过,则保存用户信息到本地session,(下次访问则无需经过登录服务器,判断session中存在用户即可),返回用户想到访问的含受保护资源页面。
1
2
3
4
5
6
java复制代码@ResponseBody
@GetMapping("/loginUserInfo")
public String loginUserInfo(@RequestParam("token") String token){
String s = redisTemplate.opsForValue().get(token);
return s;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码@GetMapping("/abc")
public String abc(HttpServletRequest request,HttpSession session, @RequestParam(value = "token",required = false) String token) throws Exception {
// 判断是否携带令牌
if (!StringUtils.isEmpty(token)){
// 携带令牌,可能是已登录用户,需向登录服务器进行确认
Map<String,String> map = new HashMap<>();
map.put("token",token);
HttpResponse response = HttpUtils.doGet("http://auth.ylogin.com", "/loginUserInfo", "GET", new HashMap<String, String>(), map);
String s = EntityUtils.toString(response.getEntity());
if (!StringUtils.isEmpty(s)){
// 验证通过,保存登录用户信息到本地session,下次访问则无需经过登录服务器
UserResponseVo userResponseVo = JSON.parseObject(s, new TypeReference<UserResponseVo>() {
});
session.setAttribute(AuthServerConstant.LOGIN_USER,userResponseVo);
localSession.put(token,session);
sessionTokenMapping.put(session.getId(),token);
}
}
UserResponseVo attribute = (UserResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null){
return "abc";
} else {
session.setAttribute("msg","请先进行登录");
return "redirect:http://auth.ylogin.com/login.html?redirectURL=http://ylogin.client1.com"+request.getServletPath();
}
}
  1. 用户再次发起请求,访问Client2中受保护的资源,同样会先到登录服务器的登录页面,但此时会带上cookie,登录服务器一看,有cookie,就知道这是一个在其他系统登录过的用户,就发放一个令牌,重定向到用户访问的地址。
1
2
3
4
5
6
7
8
9
10
11
java复制代码@GetMapping("/login.html")
public String loginPage(@RequestParam("redirectURL") String url, @CookieValue(value = "sso_token",required = false) String sso_token){
// 先判断是否在其他系统登录过
if (!StringUtils.isEmpty(sso_token)){
// 添加登录地址
addLoginUrl(url);
System.out.println("已登录");
return "redirect:"+url+"?token="+sso_token;
}
return "login";
}
  1. 应用服务器2拿到令牌,同样需要到登录服务器进行验证,验证成功则保存用户信息到本地session,返回访问资源页面。
  2. 应用服务器判断用户是否登录,第一次看是否携带令牌,之后就看本地session中有没有登录用户的信息。
  3. 登录服务器判断用户是否登录,第一次就到数据库查询,之后就看是否携带cookie。
  1. 单点登出流程

话不多说,先放个单点登出的流程图。

单点登出.png

  1. 用户点击注销按钮,携带令牌到登录服务器进行验证,同样需要携带上回调地址(一般为公共资源页面即可),作为登出后展示在浏览器的页面。

你是不是有几个疑问呢。为什么退出登录也需要携带令牌?本地session中只保存了登录用户的基本信息,那要如何携带令牌到登录服务器呢?不着急,下面就为你解答。

* 携带令牌的目的是为了验证改退出请求是登录用户发起的,防止其他人恶意请求。
* 对于获取token,我们可以利用SessionID来获取token,所以我们必须在登录成功后,保存用户信息到session的同时,也保存SessionID和token的映射关系(可以使用静态map来保存)。
1
2
java复制代码// SessionID->token
private static final Map<String, String> sessionTokenMapping = new HashMap<>();
1
2
3
4
5
6
7
java复制代码@GetMapping("/logout")
public String logout(HttpServletRequest request){
// 根据sessionId获取token令牌
String sessionId = request.getSession().getId();
String token = sessionTokenMapping.get(sessionId);
return "redirect:http://auth.ylogin.com/logOut?redirectURL=http://ylogin.client1.com&token="+token;
}
  1. 登录服务器验证成功,向已经登陆的所有应用服务器发起注销请求(带上令牌)。所以我们需要知道有哪些应用服务器登陆了。这就是我在上面剧透的,登录服务器在验证登录时保存应用服务器地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private void addLoginUrl(String url){
String s = redisTemplate.opsForValue().get("loginUrl");
if (StringUtils.isEmpty(s)){
List<String> urls = new ArrayList<>();
urls.add(url);
redisTemplate.opsForValue().set("loginUrl",JSON.toJSONString(urls));
} else{
List<String> urls = JSON.parseObject(s, new TypeReference<List<String>>() {
});
urls.add(url);
redisTemplate.opsForValue().set("loginUrl",JSON.toJSONString(urls));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码@GetMapping("/logOut")
public String logout(HttpServletRequest request, HttpServletResponse response,@RequestParam("redirectURL") String url, @RequestParam("token") String token) throws Exception {
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0){
for (Cookie cookie : cookies) {
if (cookie.getName().equals("sso_token")){
// 验证令牌
if (cookie.getValue().equals(token)){
String value = cookie.getValue();
// 清除各应用系统的session
String s = redisTemplate.opsForValue().get("loginUrl");
Map<String, String> map = new HashMap<>();
map.put("token",value);
if (!StringUtils.isEmpty(s)){
List<String> urls = JSON.parseObject(s, new TypeReference<List<String>>() {
});
for (String loginUrl : urls) {
HttpUtils.doGet(loginUrl, "/deleteSession", "GET",new HashMap<String, String>(), map);
}
}
// 删除redis中保存的用户信息
redisTemplate.delete(value);
// 清除SSO服务器的cookie令牌
Cookie cookie1 = new Cookie("sso_token", "");
cookie1.setPath("/");
cookie1.setMaxAge(0);
response.addCookie(cookie1);
}
}
}
}
// 清除redis保存的登录url
redisTemplate.delete("loginUrl");
return "redirect:"+url;
}
  1. 应用服务器收到登录服务器的注销请求,首先验证令牌,判断是否是登录服务器发起的注销请求。
1
2
3
4
5
6
7
8
java复制代码@ResponseBody
@GetMapping("/abc/deleteSession")
public String logout(@RequestParam("token") String token){
HttpSession session = localSession.get(token);
// session.removeAttribute(AuthServerConstant.LOGIN_USER);
session.invalidate();
return "logout";
}
* 这里尤其需要注意,需要获取指定session。登录服务器发送过来的请求,如果直接request.getSession().getId()获取,这样获取到的是新的session,并不是保存用户信息的会话。
* 为解决这一问题,在保存用户信息到本地session的同时,使用静态map来保存session,以令牌作为key。



1
2
java复制代码// token->session
private static final Map<String, HttpSession> localSession = new HashMap<>();

至此,单点登录功能基本实现。如果感兴趣,欢迎到我的github仓库获取源码。如果觉得有用的话,欢迎start。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%