单点登录

1. 单点登录

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

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

@Bean
public CookieSerializer cookieSerializer(){
    DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
    cookieSerializer.setDomainName("ylogin.com");
    cookieSerializer.setCookieName("YLOGINESSION");
    return cookieSerializer;
}

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

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();
        }
    }
    
  2. 浏览器展示登录页

  3. 用户输入账号密码进行登录,并在隐藏域提交回调地址

  4. 登录服务器查询数据库,验证账号及密码。账号密码正确,则生成一个令牌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();
        }
    }
    
  5. 应用服务器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();
        }
    }
    
  6. 用户再次发起请求,访问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";
    }
    
  7. 应用服务器2拿到令牌,同样需要到登录服务器进行验证,验证成功则保存用户信息到本地session,返回访问资源页面。

  8. 应用服务器判断用户是否登录,第一次看是否携带令牌,之后就看本地session中有没有登录用户的信息。

  9. 登录服务器判断用户是否登录,第一次就到数据库查询,之后就看是否携带cookie。

2. 单点登出流程

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

在这里插入图片描述

  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;
    }
    
  2. 登录服务器验证成功,向已经登陆的所有应用服务器发起注销请求(带上令牌)。所以我们需要知道有哪些应用服务器登陆了。这就是我在上面剧透的,登录服务器在验证登录时保存应用服务器地址。

    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;
    }
    
  3. 应用服务器收到登录服务器的注销请求,首先验证令牌,判断是否是登录服务器发起的注销请求。

    @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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一大岐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值