没有比这更详细的单点登录流程了

1. 单点登录

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

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

@Bean
public CookieSerializer cookieSerializer(){
   DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
   cookieSerializer.setDomainName("ylogin.com");
   cookieSerializer.setCookieName("YLOGINESSION");
   return cookieSerializer;![单点登录.png](https://upload-images.jianshu.io/upload_images/26138896-567d84ffe7300b90.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

}

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

1. 单点登录流程

单点登录流程图如下

  1. 假设现在第一次访问Client1的受保护的资源,由于我们没有登录,则需要跳转到登录服务器进行登录,但是登录之后应该跳到哪里呢?很显然,需要跳回到我们想要访问的页面,所以在重定向到登录服务器时带上回调地址redirectURL。
@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)。然后携带上令牌重定向到回调地址(即登录前页面)。

@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中存在用户即可),返回用户想到访问的含受保护资源页面。
@ResponseBody
@GetMapping("/loginUserInfo")
public String loginUserInfo(@RequestParam("token") String token){
    String s = redisTemplate.opsForValue().get(token);
    return s;
}
@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,就知道这是一个在其他系统登录过的用户,就发放一个令牌,重定向到用户访问的地址。
@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。

2. 单点登出流程

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

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

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

    • 携带令牌的目的是为了验证改退出请求是登录用户发起的,防止其他人恶意请求。

    • 对于获取token,我们可以利用SessionID来获取token,所以我们必须在登录成功后,保存用户信息到session的同时,也保存SessionID和token的映射关系(可以使用静态map来保存)。

// SessionID->token
private static final Map<String, String> sessionTokenMapping = new HashMap<>();
@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. 登录服务器验证成功,向已经登陆的所有应用服务器发起注销请求(带上令牌)。所以我们需要知道有哪些应用服务器登陆了。这就是我在上面剧透的,登录服务器在验证登录时保存应用服务器地址。
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));
    }
}
@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. 应用服务器收到登录服务器的注销请求,首先验证令牌,判断是否是登录服务器发起的注销请求。
@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。
// token->session
private static final Map<String, HttpSession> localSession = new HashMap<>();

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

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容