单点登录:流程分析及实战(二)

单点登录:流程分析及实战(二)

同系列文章
单点登录:原理及概念(一)

单点登录:流程分析及实战(二)

一、基于认证中心跨域流程

如果是不同域的情况下,Cookie是不共享的,这里我们可以部署一个认证中心,用于专门处理登录请求的独立的 Web 服务

用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 token 写入 Cookie(注意这个 Cookie 是认证中心的,应用系统是访问不到的)

应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心

由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了

如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录

如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统

应用系统拿到 Token 之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了。

二、实战

2.1、环境准备

修改本机host模拟跨域,两个客户端以及一个认证中心的域名

 127.0.0.1 www.mysso.com
 127.0.0.1 www.myclient1.com
 127.0.0.1 www.myclient2.com

2.2、SSO认证中心的职责

  • 统一登录页面和接口
  • 校验令牌是否有效
  • 校验是否登录
  • 统一退出接口销毁客户端的Session

2.3、SSO登录流程

登录流程:

登录流程

代码如下所示:

    @RequestMapping("/login")
    public String login(LoginParam loginParam, RedirectAttributes redirectAttributes,
                        HttpSession session, Model model) {
        User user = new User("admin", "123456");
        //Demo 项目此处模拟数据库账密校验
        if (!user.getUsername().equals(loginParam.getUsername()) || !user.getPassword().equals(loginParam.getPassword())) {
            model.addAttribute("msg", "账户或密码错误,请重新登录!");
            model.addAttribute("redirectUrl", loginParam.getRedirectUrl());
            return "login";
        }

        //登录成功
        //创建令牌
        String ssoToken = UUID.randomUUID().toString();
        //把令牌放到全局会话中
        session.setAttribute("ssoToken", ssoToken);
        //设置session失效时间-单位秒
        session.setMaxInactiveInterval(SSOConstantPool.expireTime);
        //将有效的令牌-放到redis容器中
        //key:token,value:用户信息
        redisService.set(SSOConstantPool.token_pool + ssoToken, user, SSOConstantPool.expireTime);

        //未携带重定向跳转地址-默认跳转到认证中心首页
        if (StringUtils.isEmpty(loginParam.getRedirectUrl())) {
            return "index";
        }

        // 携带令牌到客户端
        redirectAttributes.addAttribute("ssoToken", ssoToken);
        log.info("[ SSO登录 ] login success ssoToken:{} , sessionId:{}", ssoToken, session.getId());
        // 跳转到客户端
        return "redirect:" + loginParam.getRedirectUrl();
    }

2.4、客户端校验是否登录

流程图如下所示:

客户端

通过拦截器实现:

@Slf4j
public class WebInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        log.info("[ WebInterceptor ] >> preHandle  requestUrl:{} ", request.getRequestURI());
        //判断是否有局部会话,存在直接返回无需再次访问认证中心
        HttpSession session = request.getSession();
        //session.removeAttribute("isLogin");
        Object isLogin = session.getAttribute("isLogin");
        if (isLogin != null && (Boolean) isLogin) {
            log.debug("[ WebInterceptor ] >> 已登录,有局部会话 requestUrl:{}", request.getRequestURI());
            return true;
        }
        //获取令牌ssoToken
        String token = SSOClientHelper.getSsoToken(request);

        //无令牌
        if (StringUtils.isEmpty(token)) {
            //请求SSO------>认证中心验证是否已经登录(是否存在全局会话)
            SSOClientHelper.checkLogin(request, response);
            return true;
        }

        //请求SSO------>有令牌-则请求认证中心校验令牌是否有效
        Boolean checkToken = SSOClientHelper.checkToken(token, session.getId());

        //令牌无效,可能过期,或者token被恶意篡改
        if (!checkToken) {
            log.debug("[ WebInterceptor ] >> 令牌无效,将跳转认证中心进行认证 requestUrl:{}, token:{}", request.getRequestURI(), token);
            //请求SSO------>认证中心验证是否已经登录(是否存在全局会话)
            SSOClientHelper.checkLogin(request, response);
            return true;
        }

        //token有效,创建局部会话设置登录状态,并放行
        session.setAttribute("isLogin", true);
        //设置session失效时间-单位秒
        session.setMaxInactiveInterval(1800);
        //设置本域cookie
        CookieUtil.setCookie(response, SSOClientHelper.SSOProperty.TOKEN_NAME, token, 1800);
        log.debug("[ WebInterceptor ] >> 令牌有效,创建局部会话成功 requestUrl:{}, token:{}", request.getRequestURI(), token);
        return true;
    }

}

2.5、SSO校验令牌是否有效

@ResponseBody
    @RequestMapping("/checkToken")
    public String verify(String ssoToken, String loginOutUrl, String jsessionid) {
        // 判断token是否存在redis容器中,如果存在则代表合法
        User user = (User) redisService.get(SSOConstantPool.token_pool + ssoToken);
        boolean isVerify = user != null;
        if (!isVerify) {
            log.info("[ SSO-令牌校验 ] checkToken 令牌已失效 ssoToken:{}", ssoToken);
            return "false";
        }

        //把客户端的登出地址记录起来,后面注销的时候需要根据使用,存储在redis中
        List<Object> clientInfoList = redisService.lRange(SSOConstantPool.client_register_pool + ssoToken, 0, -1);
        boolean flag = clientInfoList.stream().anyMatch(x -> {
            ClientRegisterModel clientRegisterModel = (ClientRegisterModel) x;
            return clientRegisterModel.getJsessionid().equals(jsessionid);
        });
        if (!flag) {
            ClientRegisterModel vo = new ClientRegisterModel();
            vo.setLoginOutUrl(loginOutUrl);
            vo.setJsessionid(jsessionid);
            //redis List数据结构,push进去,并刷新过期时间
            redisService.lPush(SSOConstantPool.client_register_pool + ssoToken, vo, SSOConstantPool.expireTime);
        }
        //刷新令牌过期时间
        redisService.expire(SSOConstantPool.token_pool + ssoToken, SSOConstantPool.expireTime);
        log.info("[ SSO-令牌校验 ] checkToken success ssoToken:{} , clientInfoList:{}", ssoToken, clientInfoList);
        return "true";
    }

2.6、SSO校验是否登录

@RequestMapping("/checkLogin")
    public String checkLogin(String redirectUrl, RedirectAttributes redirectAttributes,
                             Model model, HttpServletRequest request) {
        //从认证中心-session中判断是否已经登录过(判断是否有全局会话)
        Object ssoToken = request.getSession().getAttribute("ssoToken");

        // ssoToken为空 - 没有全局回话
        if (StringUtils.isEmpty(ssoToken)) {
            log.info("[ SSO-登录校验 ] checkLogin fail 没有全局回话 ssoToken:{}", ssoToken);
            //登录成功需要跳转的地址继续传递
            model.addAttribute("redirectUrl", redirectUrl);
            //跳转到统一登录页面
            return "login";
        }

        log.info("[ SSO-登录校验 ] checkLogin success 有全局回话  ssoToken:{}", ssoToken);
        //重定向参数拼接(将会在url中拼接)
        redirectAttributes.addAttribute("ssoToken", ssoToken);
        //重定向到目标系统,后续会再次进入客户端的拦截器刷新局部令牌等
        return "redirect:" + redirectUrl;
    }

2.7、SSO退出接口

SSO退出接口较为简单,只是清除SSO中的全局会话。

具体的销毁工作是通过session的监听器来实现的。

流程如下:

在这里插入图片描述

    /**
     * 统一注销
     * 1.注销全局会话
     * 2.通过监听全局会话session时效性,向已经注册的所有子系统发起注销请求
     */
    @RequestMapping("/logOut")
    public String logOut(HttpServletRequest request) {
        HttpSession session = request.getSession();
        log.info("[ SSO-统一退出 ] ....start.... sessionId:{}", session.getId());
        //注销全局会话, SSOSessionListener 监听器会处理后续操作
        request.getSession().invalidate();
        log.info("[ SSO-统一退出 ] ....end.... sessionId:{}", session.getId());
        return "logout";
    }

2.8、销毁Session的监听器

实现HttpSessionListener接口,servlet销毁session的时候会执行所有的监听器。

@Override
    public void sessionDestroyed(HttpSessionEvent se) {
        //获取操作redis的单例
        RedisService redisService = SpringContextUtils.getBean(RedisServiceImpl.class);
        HttpSession session = se.getSession();
        String token = (String) session.getAttribute("ssoToken");
        log.debug("[ SSOSessionListener ] ...start..... sessionId:{},token:{}", session.getId(), token);
        //注销全局会话,SSOSessionListener监听类删除对应的信息
        session.invalidate();
        if (StringUtils.isEmpty(token)) {
            log.debug("[ SSOSessionListener ] token is null sessionId:{}", session.getId());
            return;
        }
        //清除存储的有效token数据
        redisService.del(SSOConstantPool.token_pool + token);
        //清除并返回已经注册的系统信息,调用客户端退出接口
        List<Object> list = redisService.lRange(SSOConstantPool.client_register_pool + token, 0, -1);
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
		//循环调用
        for (Object object : list) {
            if (null == object) {
                continue;
            }
            ClientRegisterModel client = (ClientRegisterModel) object;
            //取出注册的子系统,依次调用子系统的登出方法(通过会话ID退出子系统的局部会话)
            sendHttpRequest(client.getLoginOutUrl(), client.getJsessionid());
            log.info("[ SSOSessionListener ] 注销系统 url:{},Jsessionid:{}", client.getLoginOutUrl(), client.getJsessionid());
        }
        redisService.del(SSOConstantPool.client_register_pool + token);
        log.debug("[ SSOSessionListener ] ...end..... sessionId:{},token:{}", session.getId(), token);
    }

sendHttpRequest方法调用:

    /**
     * 发送退出登录请求
     * 模拟浏览器访问形式
     *
     * @param reqUrl     发送请求的地址
     * @param jesssionId 会话Id
     */
    private static void sendHttpRequest(String reqUrl, String jesssionId) {
        try {
            //建立URL连接对象
            URL url = new URL(reqUrl);
            //创建连接
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            //设置请求的方式(需要是大写的)
            conn.setRequestMethod("POST");
            //设置需要响应结果
            conn.setDoOutput(true);
            //通过设置JSESSIONID模拟浏览器端操作
            conn.addRequestProperty("Cookie", "JSESSIONID=" + jesssionId);
            //发送请求到服务器
            conn.connect();
            conn.getInputStream();
            conn.disconnect();
        } catch (Exception e) {
            log.error("[ sendHttpRequest ] exception >> reqUrl:{}", reqUrl, e);
        }
    }

2.9、客户端退出登录接口

    @RequestMapping("/logOut")
    protected void logOut(HttpServletRequest request, HttpServletResponse response) {
        HttpSession session = request.getSession();
        log.info("[ logOut ] sessionId:{}", session.getId());
        //清空客户端的局部会话
        session.invalidate();
    }

三、测试实现

依次访问如下地址

www.myclient1.com:8082
www.myclient2.com:8083

1、访问之后会自动重定向到SSO的统一认证,且后面带着客户端的访问地址

在这里插入图片描述

2、客户端一登录之后,刷新客户端二的页面,发现已经自动登录。

在这里插入图片描述

观察redis保存的token以及客户端页面的sessioncookie

在这里插入图片描述

在这里插入图片描述

3、任意客户端退出,刷新另一个客户端的页面都会重定向到登录页面。

测试成功!!!

当然生成环境中记得各种令牌以及系统之间调用进行映射以及加密使用。

参考:
https://xqiangme.blog.csdn.net/article/details/113695587

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值