深度剖析分散式單點登錄框架XXL-SSO

於2018年初,在github上創建XXL-SSO項目倉庫並提交第一個commit,隨之進行系統結構設計,UI選型,交互設計……

於2018年初,在github上創建XXL-SSO項目倉庫並提交第一個commit,隨之進行系統結構設計,UI選型,交互設計……

於2018-12-05,XXL-SSO參與”2018年度最受歡迎中國開源軟體“評比,在當時已錄入的一萬多個國產開源項目中角逐,最終排名第55名。

於2019-01-23,XXL-SSO被評選上榜”2018年度新增開源軟體排行榜之國產 TOP 50“評比,排名第8名。

至今,XXL-SSO已接入多家公司的線上產品線,接入場景如電商業務,O2O業務和核心中間件配置動態化等,截止2018-03-15為止,XXL-SSO已接入的公司包括不限於:

  1. 湖南創發科技
  2. 深圳龍華科技有限公司
  3. 摩根國際
  4. 印記雲

一、簡介

1.1 概述

XXL-SSO 是一個分散式單點登錄框架。只需要登錄一次就可以訪問所有相互信任的應用系統。
擁有”輕量級、分散式、跨域、Cookie+Token均支援、Web+APP均支援”等特性。現已開放源程式碼,可以做到開箱即用。

1.2 特性

  • 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客戶端不需要過濾的路徑;

1.3 下載

文檔地址

源碼倉庫地址

源碼倉庫地址 Release Download
//github.com/xuxueli/xxl-sso Download
//gitee.com/xuxueli0323/xxl-sso Download

技術交流

1.4 環境

  • JDK:1.7+********
  • Redis:4.0+
  • Mysql:5.6+

二、快速入門(基於Cookie)

2.1 系統資料庫初始化

2.2 源碼編譯

- 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被禁用等,springboot版本

2.3 部署 “認證中心(SSO Server)”

項目名:xxl-sso-server

配置說明

配置文件位置:application.properties

……

// redis 地址: 如 "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";多地址逗號分隔
xxl.sso.redis.address=redis://127.0.0.1:6379

// 登錄態有效期窗口,默認24H,當登錄態有效期窗口過半時,自動順延一個周期;
xxl.sso.redis.expire.minite=1440

Redis 密碼配置

xxl-sso-core項目中com.xxl.sso.core.util包下的JedisUtil類中大約85行設置密碼,如若redis沒有設置密碼,則不需要配置此處。

for (int i = 0; i < addressArr.length; i++) {
	JedisShardInfo jedisShardInfo = new JedisShardInfo(addressArr[i]);
    jedisShardInfo.setPassword("******");#添加密碼
    jedisShardInfos.add(jedisShardInfo);
}

2.4 部署 “單點登陸Client端接入示例項目”

項目名:xxl-sso-web-sample-springboot

maven依賴

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

配置 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;
}

配置說明

配置文件位置:application.properties

……

### xxl-sso     (CLient端SSO配置)

##### SSO Server認證中心地址(推薦以域名方式配置認證中心,本機可參考章節"2.5"修改host文件配置域名指向)
xxl.sso.server=//xxlssoserver.com:8080/xxl-sso-server

##### 註銷登陸path,值為Client端應用的相對路徑
xxl.sso.logout.path=/logout

##### 路徑排除Path,允許設置多個,且支援Ant表達式。用於排除SSO客戶端不需要過濾的路徑
xxl-sso.excluded.paths=

### redis   // redis address, like "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";Multiple "," separated
xxl.sso.redis.address=redis://xxl-sso:[email protected]:6379/0

路徑過濾:在xxl-sso-core項目中com.xxl.sso.core.filer包下的XxlSsoWebFilter類中大約54行

 // excluded path check
        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;
                }

            }
        }

判斷登錄重定向:在xxl-sso-core項目中com.xxl.sso.core.filer包下的XxlSsoWebFilter類中大約82行

// valid login fail
        if (xxlUser == null) {

            String header = req.getHeader("content-type");
            boolean isJson=  header!=null && header.contains("json");
            if (isJson) {

                // json msg
                res.setContentType("application/json;charset=utf-8");
                res.getWriter().println("{\"code\":"+Conf.SSO_LOGIN_FAIL_RESULT.getCode()+", \"msg\":\""+ Conf.SSO_LOGIN_FAIL_RESULT.getMsg() +"\"}");
                return;
            } else {

                // total link
                String link = req.getRequestURL().toString();

                // redirect logout
                String loginPageUrl = ssoServer.concat(Conf.SSO_LOGIN)
                        + "?" + Conf.REDIRECT_URL + "=" + link;

                res.sendRedirect(loginPageUrl);
                return;
            }

        }

2.5 驗證

  • 環境準備:啟動Redis、初始化Mysql表數據;

  • 修改Host文件:域名方式訪問認證中心,模擬跨域與線上真實環境

### 在host文件中添加以下內容0
127.0.0.1 xxlssoserver.com
127.0.0.1 xxlssoclient1.com
127.0.0.1 xxlssoclient2.com
  • 分別運行 “xxl-sso-server” 與 “xxl-sso-web-sample-springboot”
    1、SSO認證中心地址:
    //xxlssoserver.com:8080/xxl-sso-server
    
    2、Client01應用地址:
    //xxlssoclient1.com:8081/xxl-sso-web-sample-springboot/
    
    3、Client02應用地址:
    //xxlssoclient2.com:8081/xxl-sso-web-sample-springboot/
  • SSO登錄/註銷流程驗證
    正常情況下,登錄流程如下:
    1、訪問 "Client01應用地址" ,將會自動 redirect 到 "SSO認證中心地址" 登錄介面;
    2、成功登錄後,將會自動 redirect 返回到 "Client01應用地址",並切換為已登錄狀態;
    3、此時,訪問 "Client02應用地址",不需登陸將會自動切換為已登錄狀態;
    
    正常情況下,註銷流程如下:
    1、訪問 "Client01應用地址" 配置的 "註銷登陸path",將會自動 redirect 到 "SSO認證中心地址" 並自動註銷登陸狀態;
    2、此時,訪問 "Client02應用地址",也將會自動註銷登陸狀態;


““

2.5 登錄源碼分析

 @RequestMapping("/doLogin")
    public String doLogin(HttpServletRequest request,
                        HttpServletResponse response,
                        RedirectAttributes redirectAttributes,
                        String username,
                        String password,
                        String ifRemember) {

        boolean ifRem = (ifRemember!=null&&"on".equals(ifRemember))?true:false;//判斷是否記住密碼

        // valid login
        ReturnT<UserInfo> result = userService.findUser(username, password);//校驗密碼是否正確
        if (result.getCode() != ReturnT.SUCCESS_CODE) {
            redirectAttributes.addAttribute("errorMsg", result.getMsg());

            redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));
            return "redirect:/login";
        }

        // 1、make xxl-sso user   根據不同規則設置用戶資訊
        XxlSsoUser xxlUser = new XxlSsoUser();
        xxlUser.setUserid(String.valueOf(result.getData().getUserid()));
        xxlUser.setUsername(result.getData().getUsername());
        xxlUser.setVersion(UUID.randomUUID().toString().replaceAll("-", ""));
        xxlUser.setExpireMinute(SsoLoginStore.getRedisExpireMinute());
        xxlUser.setExpireFreshTime(System.currentTimeMillis());


        // 2、make session id  根據用戶資訊生成sessionId
        String sessionId = SsoSessionIdHelper.makeSessionId(xxlUser);

        // 3、login, store storeKey + cookie sessionId  將sessionId&用戶資訊存入Reids
        SsoWebLoginHelper.login(response, sessionId, xxlUser, ifRem);

        // 4、return, redirect sessionId  重定向到子系統(並傳遞xxl_sso_sessionid)
        String redirectUrl = request.getParameter(Conf.REDIRECT_URL);
        if (redirectUrl!=null && redirectUrl.trim().length()>0) {
            String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;
            return "redirect:" + redirectUrlFinal;
        } else {
            return "redirect:/";
        }

    }

校驗密碼是否正確

@Override
    public ReturnT<UserInfo> findUser(String username, String password) {

        if (username==null || username.trim().length()==0) {
            return new ReturnT<UserInfo>(ReturnT.FAIL_CODE, "Please input username.");
        }
        if (password==null || password.trim().length()==0) {
            return new ReturnT<UserInfo>(ReturnT.FAIL_CODE, "Please input password.");
        }

        // mock user
        for (UserInfo mockUser: mockUserList) {
            if (mockUser.getUsername().equals(username) && mockUser.getPassword().equals(password)) {
                return new ReturnT<UserInfo>(mockUser);
            }
        }

        return new ReturnT<UserInfo>(ReturnT.FAIL_CODE, "username or password is invalid.");
    }

根據用戶資訊生成sessionId

public static String makeSessionId(XxlSsoUser xxlSsoUser){
        String sessionId = xxlSsoUser.getUserid().concat("_").concat(xxlSsoUser.getVersion());
        return sessionId;
    }

將sessionId&用戶資訊存入Reids

 public static void login(HttpServletResponse response,
                             String sessionId,
                             XxlSsoUser xxlUser,
                             boolean ifRemember) {

        String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId);
        if (storeKey == null) {
            throw new RuntimeException("parseStoreKey Fail, sessionId:" + sessionId);
        }

        SsoLoginStore.put(storeKey, xxlUser);
        CookieUtil.set(response, Conf.SSO_SESSIONID, sessionId, ifRemember);//在認證授權系統下存放對應cookie資訊
    }
 public static XxlSsoUser loginCheck(HttpServletRequest request, HttpServletResponse response){

        String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID);

        // cookie user
        XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(cookieSessionId);
        if (xxlUser != null) {
            return xxlUser;
        }

        // redirect user

        // remove old cookie
        SsoWebLoginHelper.removeSessionIdByCookie(request, response);

        // set new cookie
        String paramSessionId = request.getParameter(Conf.SSO_SESSIONID);
        xxlUser = SsoTokenLoginHelper.loginCheck(paramSessionId);
        if (xxlUser != null) {
            CookieUtil.set(response, Conf.SSO_SESSIONID, paramSessionId, false);    // expire when browser close (client cookie)
            return xxlUser;
        }

        return null;
    }

三、快速入門(基於Token)

3.1 “認證中心(SSO Server)” 搭建

“認證中心” 搭建成功後,默認為Token方式登陸提供API介面如下:

  • 1、登陸介面:/app/login

    • 參數:POST參數
      • username:帳號
      • password:帳號
    • 響應:JSON格式
      • code:200 表示成功、其他失敗;
      • msg:錯誤提示
      • data: 登陸用戶的 sso sessionid
  • 2、註銷介面:/app/logout

    • 參數:POST參數
      • sessionId:登陸用戶的 sso sessionid
    • 響應:JSON格式
      • code:200 表示成功、其他失敗;
      • msg:錯誤提示
  • 3、登陸狀態校驗介面:/app/logincheck

    • 參數:POST參數
      • sessionId:登陸用戶的 sso sessionid
    • 響應:JSON格式
      • code:200 表示成功、其他失敗;
      • msg:錯誤提示
      • data:登陸用戶資訊
        • userid:用戶ID
        • username:用戶名

2.2 部署 “單點登陸Client端接入示例項目” (Token方式)

項目名:xxl-sso-token-sample-springboot

2.3 驗證 (模擬請求 Token 方式接入SSO的介面)

  • 環境準備:啟動Redis、初始化Mysql表數據;

  • 修改Host文件:域名方式訪問認證中心,模擬跨域與線上真實環境

### 在host文件中添加以下內容0
127.0.0.1 xxlssoserver.com
127.0.0.1 xxlssoclient1.com
127.0.0.1 xxlssoclient2.com
  • 分別運行 “xxl-sso-server” 與 “xxl-sso-token-sample-springboot”
    1、SSO認證中心地址:
    //xxlssoserver.com:8080/xxl-sso-server
    
    2、Client01應用地址:
    //xxlssoclient1.com:8082/xxl-sso-token-sample-springboot/
    
    3、Client02應用地址:
    //xxlssoclient2.com:8082/xxl-sso-token-sample-springboot/
  • SSO登錄/註銷流程驗證

可參考測試用例 :com.xxl.app.sample.test.TokenClientTest

    正常情況下,登錄流程如下:
    1、獲取用戶輸入的帳號密碼後,請求SSO Server的登錄介面,獲取用戶 sso sessionid ;(參考程式碼:TokenClientTest.loginTest)
    2、登陸成功後,獲取到 sso sessionid ,需要主動存儲,後續請求時需要設置在 Header參數 中;
    3、此時,使用 sso sessionid 訪問受保護的 "Client01應用" 和 "Client02應用" 提供的介面,介面均正常返回;(參考程式碼:TokenClientTest.clientApiRequestTest)
    
    正常情況下,註銷流程如下:
    1、請求SSO Server的註銷介面,註銷登陸憑證 sso sessionid ;(參考程式碼:TokenClientTest.logoutTest)
    2、註銷成功後,sso sessionid 將會全局失效;
    3、此時,使用 sso sessionid 訪問受保護的 "Client01應用" 和 "Client02應用" 提供的介面,介面請求將會被攔截,提示未登錄並返回狀態碼 501 ;(參考程式碼:TokenClientTest.clientApiRequestTest)

四、總體設計

4.1 架構圖

4.2 功能定位

XXL-SSO 是一個分散式單點登錄框架。只需要登錄一次就可以訪問所有相互信任的應用系統。

藉助 XXL-SSO,可以快速實現分散式系統單點登錄。

4.3 核心概念

概念 說明
SSO Server 中央認證服務,支援集群;
SSO Client 接入SSO認證中心的Client應用;
SSO SessionId 登錄用戶會話ID,SSO 登錄成功為用戶自動分配;
SSO User 登錄用戶資訊,與 SSO SessionId 相對應;

4.4 登錄流程剖析

  • 用戶於Client端應用訪問受限資源時,將會自動 redirect 到 SSO Server 進入統一登錄介面。
  • 用戶登錄成功之後將會為用戶分配 SSO SessionId 並 redirect 返回來源Client端應用,同時附帶分配的 SSO SessionId。
  • 在Client端的SSO Filter里驗證 SSO SessionId 無誤,將 SSO SessionId 寫入到用戶瀏覽器Client端域名下 cookie 中。
  • SSO Filter驗證 SSO SessionId 通過,受限資源請求放行;

4.5 註銷流程剖析

  • 用戶與Client端應用請求註銷Path時,將會 redirect 到 SSO Server 自動銷毀全局 SSO SessionId,實現全局銷毀;
  • 然後,訪問接入SSO保護的任意Client端應用時,SSO Filter 均會攔截請求並 redirect 到 SSO Server 的統一登錄介面。

4.6 基於Cookie,相關概念

  • 登陸憑證存儲:登陸成功後,用戶登陸憑證被自動存儲在瀏覽器Cookie中;

  • Client端校驗登陸狀態:通過校驗請求Cookie中的是否包含用戶登錄憑證判斷;

  • 系統角色模型:

    • SSO Server:認證中心,提供用戶登陸、註銷以及登陸狀態校驗等功能。
    • Client應用:受SSO保護的Client端Web應用,為用戶瀏覽器訪問提供服務;
    • 用戶:發起請求的用戶,使用瀏覽器訪問。

4.7 基於Token,相關概念

  • 登陸憑證存儲:登陸成功後,獲取到登錄憑證(xxl_sso_sessionid=xxx),需要主動存儲,如存儲在 localStorage、Sqlite 中;
  • Client端校驗登陸狀態:通過校驗請求 Header參數 中的是否包含用戶登錄憑證(xxl_sso_sessionid=xxx)判斷;因此,發送請求時需要在 Header參數 中設置登陸憑證;
  • 系統角色模型:
    • SSO Server:認證中心,提供用戶登陸、註銷以及登陸狀態校驗等功能。
    • Client應用:受SSO保護的Client端Web應用,為用戶請求提供介面服務;
    • 用戶:發起請求的用戶,如使用Android、IOS、桌面客戶端等請求訪問。

4.8 未登錄狀態請求處理

基於Cookie,未登錄狀態請求:

  • 頁面請求:redirect 到SSO Server登錄介面;
  • JSON請求:返回未登錄的JSON格式響應數據
    • 數據格式:
      • code:501 錯誤碼
      • msg:sso not login.

基於Token,未登錄狀態請求:

  • 返回未登錄的JSON格式響應數據
    • 數據格式:
      • code:501 錯誤碼
      • msg:sso not login.

4.9 登錄態自動延期

支援自定義登錄態有效期窗口,默認24H,當登錄態有效期窗口過半時,自動順延一個周期;

4.10 記住密碼

未記住密碼時,關閉瀏覽器則登錄態失效;記住密碼時,登錄態自動延期,在自定義延期時間的基礎上,原則上可以無限延期;

4.11 路徑排除

自定義路徑排除Path,允許設置多個,且支援Ant表達式。用於排除SSO客戶端不需要過濾的路徑


Tags: