草庐IT

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

雨点的名字 2023-03-28 原文

有关单点登录(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客户端不需要过滤的路径

2、环境

  • 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、 架构图

应用系统: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/

3、验证

SSO登录流程

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

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

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

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

很明显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原稿和原图片。其它情况一律禁止转载!

有关推荐一个分布式单点登录框架XXL-SSO!的更多相关文章

  1. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  2. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  3. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  4. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  5. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  6. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  7. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  8. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

  9. ruby - 分布式事务和队列,ruby,erlang,scala - 2

    我有一个涉及多台机器、消息队列和事务的问题。因此,例如用户点击网页,点击将消息发送到另一台机器,该机器将付款添加到用户的帐户。每秒可能有数千次点击。事务的所有方面都应该是容错的。我以前从未遇到过这样的事情,但一些阅读表明这是一个众所周知的问题。所以我的问题。我假设安全的方法是使用两阶段提交,但协议(protocol)是阻塞的,所以我不会获得所需的性能,我是否正确?我通常写Ruby,但似乎Redis之类的数据库和Rescue、RabbitMQ等消息队列系统对我的帮助不大——即使我实现某种两阶段提交,如果Redis崩溃,数据也会丢失,因为它本质上只是内存。所有这些让我开始关注erlang和

  10. ruby-on-rails - Rails 中的推荐引擎 - 2

    我想为我的Rails网络应用程序提供推荐功能。特别是,我想向新注册的用户推荐他可能想要关注的其他用户。Rails中是否有用于此目的的引擎/gem?如果没有,我应该从哪里开始构建它?谢谢。 最佳答案 有Coletivogemhttps://github.com/diogenes/coletivo我试了一下。在MySQL上运行。Neo4jhttp://neo4j.org真的很容易实现一个“跟随谁”。事实上,大多数展示其能力的样本都涉及“跟随谁”。快速提示-只有在JRuby上运行时,Neo4j.rb才会很酷。如果不是-使用Neograph

随机推荐