推薦一個分散式單點登入框架XXL-SSO!

雨點的名字發表於2023-02-22

有關單點登入(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、 架構圖

推薦一個分散式單點登入框架XXL-SSO!

應用系統: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認證中心地址" 登入介面

推薦一個分散式單點登入框架XXL-SSO!

2、成功登入後,將會自動 redirect 返回到 "Client01應用地址",並切換為已登入狀態

推薦一個分散式單點登入框架XXL-SSO!

3、此時,訪問 "Client02應用地址",不需登陸將會自動切換為已登入狀態

推薦一個分散式單點登入框架XXL-SSO!

很明顯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原稿和原圖片。其它情況一律禁止轉載!

相關文章