3

推荐一个分布式单点登录框架XXL-SSO! - 雨点的名字

 1 year ago
source link: https://www.cnblogs.com/qdhxhz/p/17043081.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

有关单点登录(SSO)之前有写过两篇文章

如果说XXL-JOB你可能并不陌生,它是非常火爆的一个分布式任务调度平台。但其实在该作者还有一个非常优秀的开源项目叫XXL-SSO,这两个个项目都是1000+Star。

XXL-SSO 是一个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。 拥有"轻量级、分布式、跨域、Cookie+Token均支持、Web+APP均支持"等特性。现已开放源代码,开箱即用。

这里主要是通过对XXL-SSO源码的分析,将理论和实践结合!

一、快速接入sso

1、xxl-sso特性

  1. 简洁:API直观简洁,可快速上手
  2. 轻量级:环境依赖小,部署与接入成本较低
  3. 单点登录:只需要登录一次就可以访问所有相互信任的应用系统
  4. 分布式:接入SSO认证中心的应用,支持分布式部署
  5. HA:Server端与Client端,均支持集群部署,提高系统可用性
  6. 跨域:支持跨域应用接入SSO认证中心
  7. Cookie+Token均支持:支持基于Cookie和基于Token两种接入方式,并均提供Sample项目
  8. Web+APP均支持:支持Web和APP接入
  9. 实时性:系统登陆、注销状态,全部Server与Client端实时共享
  10. CS结构:基于CS结构,包括Server"认证中心"与Client"受保护应用"
  11. 记住密码:未记住密码时,关闭浏览器则登录态失效;记住密码时,支持登录态自动延期,在自定义延期时间的基础上,原则上可以无限延期
  12. 路径排除:支持自定义多个排除路径,支持Ant表达式,用于排除SSO客户端不需要过滤的路径
  • JDK:1.7+
  • Redis:4.0+

3、 源码地址

4、 项目结构说明

- xxl-sso-server:中央认证服务,支持集群
- xxl-sso-core:Client端依赖
- xxl-sso-samples:单点登陆Client端接入示例项目
    - xxl-sso-web-sample-springboot:基于Cookie接入方式,供用户浏览器访问,springboot版本
    - xxl-sso-token-sample-springboot:基于Token接入方式,常用于无法使用Cookie的场景使用,如APP、Cookie被禁

5、 架构图

1090617-20230111103141081-1797666447.jpg

应用系统:sso-web系统(8081端口)、sso-web系统(8082端口)(需要登录的系统)

SSO客户端:登录、退出(独立jar包给应用系统引用)

SSO服务端:登录(登录服务)、登录状态(提供登录状态校验/登录信息查询的服务)、退出(用户注销服务)

数据库:存储用户账户信息(一般使用Mysql,在当前项目中为了简便并没有查询数据库)

缓存:存储用户的登录信息(使用Redis)

二、快速接入XXL-SSO框架

1、 部署认证中心(sso-server)

只需要修改配置文件即可,配置文件位置:application.properties

## 配置redis
xxl.sso.redis.address=redis://118.31.224.65:6379
##  登录态有效期窗口,默认24H,当登录态有效期窗口过半时,自动顺延一个周期
xxl.sso.redis.expire.minute=1440

2、 部署'单点登陆Client端接入示例项目'

这里指需要接入SSO的系统,在当前项目有xxl-sso-web-sample-springboot和 xxl-sso-token-sample-springboot两个示例项目,这里暂且以sso-web为示例。

1)、maven依赖

<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-sso-core</artifactId>
    <version>${最新稳定版}</version>
</dependency>

2)配置 XxlSsoFilter

参考代码:com.xxl.sso.sample.config.XxlSsoConfig

@Bean
public FilterRegistrationBean xxlSsoFilterRegistration() {
    // xxl-sso, redis init
    JedisUtil.init(xxlSsoRedisAddress);
    // xxl-sso, filter init
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setName("XxlSsoWebFilter");
    registration.setOrder(1);
    registration.addUrlPatterns("/*");
    registration.setFilter(new XxlSsoWebFilter());
    registration.addInitParameter(Conf.SSO_SERVER, xxlSsoServer);
    registration.addInitParameter(Conf.SSO_LOGOUT_PATH, xxlSsoLogoutPath);
    return registration;
}
  1. application.properties修改
## 中央认证服务地址
xxl.sso.server=http://ssoserver.com:8080/xxl-sso-server
## 退出接口
xxl.sso.logout.path=/logout
## 排除走sso的接口
xxl-sso.excluded.paths=/excludedUrl
## redis地址
xxl.sso.redis.address=redis://118.11.214.65:6379

三、快速验证

1、修改host文件

修改Host文件:域名方式访问认证中心,模拟跨域与线上真实环境

127.0.0.1       ssoserver.com
127.0.0.1       webb.com
127.0.0.1       weba.com

2、启动项目

分别运行 “xxl-sso-server” 与 “xxl-sso-web-sample-springboot”,为了验证单点登录,这里sso-web需求启动两次,只是一次是8081端口,一次是8082端口。

## 1、SSO认证中心地址:
http://ssoserver.com:8080/xxl-sso-server
## 2、Client01应用地址:
http://weba.com:8081/xxl-sso-web-sample-springboot/
## 3、Client02应用地址:
http://webb.com:8082/xxl-sso-web-sample-springboot/

SSO登录流程

正常情况下,登录流程如下:

1、访问 "Client01应用地址" ,将会自动 redirect 到 "SSO认证中心地址" 登录界面

1090617-20230111103153915-591009657.jpg

2、成功登录后,将会自动 redirect 返回到 "Client01应用地址",并切换为已登录状态

1090617-20230111103202652-1684249612.jpg

3、此时,访问 "Client02应用地址",不需登陆将会自动切换为已登录状态

1090617-20230111103211749-2068856463.jpg

很明显Client01登录成功后,Client02无需再重新登录就可以访问了。

SSO注销流程

正常情况下,注销流程如下:

1、访问 "Client01应用地址" 配置的 "注销登陆path",将会自动 redirect 到 "SSO认证中心地址" 并自动注销登陆状态
2、此时,访问 "Client02应用地址",也将会自动注销登陆状态

四、核心代码分析

1、SSO客户端(sso-core)拦截器

主要看sso拦截器流程就可以了

 @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        // 获取当前请求接口
        String servletPath = req.getServletPath();
        // 1、是否是排除 不走sso的接口 如果是直接放行
        if (excludedPaths != null && excludedPaths.trim().length() > 0) {
            for (String excludedPath : excludedPaths.split(",")) {
                String uriPattern = excludedPath.trim();

                // 支持ANT表达式
                if (antPathMatcher.match(uriPattern, servletPath)) {
                    // excluded path, allow
                    chain.doFilter(request, response);
                    return;
                }
            }
        }
        // 2、是否是退出登录接口 
        if (logoutPath != null
                && logoutPath.trim().length() > 0
                && logoutPath.equals(servletPath)) {

            // 2.1删除当前请求客户端的cookie
            SsoWebLoginHelper.removeSessionIdByCookie(req, res);
            // 2.2重定向到sso认证服务的 退出接口
            String logoutPageUrl = ssoServer.concat(Conf.SSO_LOGOUT);
            res.sendRedirect(logoutPageUrl);
            return;
        }
        // 3、校验用户(是否存在,是否过期) 这个方法下面在展开
        XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(req, res);
        // 4、令牌校验失败
        if (xxlUser == null) {
            //获取当前请求地址
            String link = req.getRequestURL().toString();
            // 重定向到sso认证服务的 登录接口
            String loginPageUrl = ssoServer.concat(Conf.SSO_LOGIN)
                    + "?" + Conf.REDIRECT_URL + "=" + link;
            res.sendRedirect(loginPageUrl);
            return;
        }
        // ser sso user
        request.setAttribute(Conf.SSO_USER, xxlUser);
        // 已经登录 放行
        chain.doFilter(request, response);
        return;
    }

在看下上面的loginCheck方法

   /**
     * 令牌校验
     *
     * @return 用户信息
     */
    public static XxlSsoUser loginCheck(HttpServletRequest request, HttpServletResponse response){

        //去cookie去获取xxl_sso_sessionid 其实就是之前原理篇说的token,只是名称叫法不同
        String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID);
        
        // 这里去redis中获取用户信息 有可能获取不到。这个方法就不粘贴了 这里有三种情况
        //1、cookieSessionId为空 那么直接返回null
        //2、cookieSessionId不为空,但在redis获取不到用户信息,因为存在其它系统退出后 redis删除了
        //3、redis获取到了用户信息,但超过有效期了 依旧返回null
        XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(cookieSessionId);
        if (xxlUser != null) {
            return xxlUser;
        }
        // 如果获取不到 所以已经在其它系统退出登录了 那删除cookie中的xxl_sso_sessionid
        SsoWebLoginHelper.removeSessionIdByCookie(request, response);
        //如果是 sso登录成功后 回调过来的 这个时候在这里是可以获取到xxl_sso_sessionid的
        String paramSessionId = request.getParameter(Conf.SSO_SESSIONID);
        xxlUser = SsoTokenLoginHelper.loginCheck(paramSessionId);
        if (xxlUser != null) {
            CookieUtil.set(response, Conf.SSO_SESSIONID, paramSessionId, false);    /
            return xxlUser;
        }
        return null;
    }

2、认证服务器(sso-server)登录接口

  /**
     * sso认证中心 登录接口
     */
    @RequestMapping(Conf.SSO_LOGIN)
    public String login(Model model, HttpServletRequest request, HttpServletResponse response) {

        // 同样的 该判断sso上有没有全局会话 
        XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response);
        //如果 其它系统登录成功过 这个就不回为null 直接再带上xxl_sso_sessionid=xxx 重定向到之前接口
        //也不用在登录了
        if (xxlUser != null) {
            // success redirect
            String redirectUrl = request.getParameter(Conf.REDIRECT_URL);
            if (redirectUrl!=null && redirectUrl.trim().length()>0) {

                String sessionId = SsoWebLoginHelper.getSessionIdByCookie(request);
                String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;;
                return "redirect:" + redirectUrlFinal;
            } else {
                return "redirect:/";
            }
        }
        //只有全局会话不存在 才会跳转登录页面 
        model.addAttribute("errorMsg", request.getParameter("errorMsg"));
        model.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));
        return "login";
    }

3、认证服务器(sso-server)退出接口

 @RequestMapping(Conf.SSO_LOGOUT)
    public String logout(HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes) {
        // 退出操作
        SsoWebLoginHelper.logout(request, response);
        // 跳转到登录页
        redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));
        return "redirect:/login";
    }

再来看下 logout 方法做了哪些事情

 public static void logout(HttpServletRequest request,
                              HttpServletResponse response) {

        String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID);
        if (cookieSessionId==null) {
            return;
        }
        //1、删除全局缓存 redis中 清除cookieSessionId,这样其它系统在令牌校验的时候 会失败。即一处退出,处处退出。
        String storeKey = SsoSessionIdHelper.parseStoreKey(cookieSessionId);
        if (storeKey != null) {
            SsoLoginStore.remove(storeKey);
        }
        //2、清除全局会话 
        CookieUtil.remove(request, response, Conf.SSO_SESSIONID);
    }

整个核心代码的逻辑都在这里了,其实结合上一篇的理论篇,理解起来就一点也不复杂了。

声明: 公众号如需转载该篇文章,发表文章的头部一定要 告知是转至公众号: 后端元宇宙。同时也可以问本人要markdown原稿和原图片。其它情况一律禁止转载!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK