深度源碼分析——XXL-SSO分佈式單點登陸框架(輕量級、分佈式、跨域、Cookie+Token、Web+APP均支持)

什麼是XXL-SSO

XXL-SSO 是一個分佈式單點登陸框架。只須要登陸一次就能夠訪問全部相互信任的應用系統。 擁有"輕量級、分佈式、跨域、Cookie+Token均支持、Web+APP均支持"等特性。現已開放源代碼,開箱即用。web

特性

  • 一、簡潔:API直觀簡潔,可快速上手
  • 二、輕量級:環境依賴小,部署與接入成本較低
  • 三、單點登陸:只須要登陸一次就能夠訪問全部相互信任的應用系統
  • 四、分佈式:接入SSO認證中心的應用,支持分佈式部署
  • 五、HA:Server端與Client端,均支持集羣部署,提升系統可用性
  • 六、跨域:支持跨域應用接入SSO認證中心
  • 七、Cookie+Token均支持:支持基於Cookie和基於Token兩種接入方式,並均提供Sample項目
  • 八、Web+APP均支持:支持Web和APP接入
  • 九、實時性:系統登錄、註銷狀態,所有Server與Client端實時共享
  • 十、CS結構:基於CS結構,包括Server"認證中心"與Client"受保護應用"
  • 十一、記住密碼:未記住密碼時,關閉瀏覽器則登陸態失效;記住密碼時,支持登陸態自動延期,在自定義延期時間的基礎上,原則上能夠無限延期
  • 十二、路徑排除:支持自定義多個排除路徑,支持Ant表達式,用於排除SSO客戶端不須要過濾的路徑

源碼編譯

- xxl-sso-server:中央認證服務,支持集羣redis

- xxl-sso-core:Client端依賴spring

- xxl-sso-samples:單點登錄Client端接入示例項目數據庫

       - xxl-sso-web-sample-springboot:基於Cookie接入方式,供用戶瀏覽器訪問,springboot版本跨域

       - xxl-sso-token-sample-springboot:基於Token接入方式,經常使用於沒法使用Cookie的場景使用,如APP、Cookie被禁用等, springboot版本瀏覽器

               

導入ideaspringboot

先啓動xxl-sso-servercookie

啓動以前先看配置文件有redis鏈接信息session

因此先啓動本地redis服務,但沒發現redis的密碼配置,密碼配置寫在哪裏呢,咱們先啓動項目看看架構

若是你本地沒有配置redis密碼,則正常啓動,配置了則啓動報錯,我本地redis沒有配置密碼因此能夠正常啓動

咱們先從config開始斷點調試

F8進入

咱們就能夠發現password在這裏配置

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

### 在host文件中添加如下內容

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"

一、SSO認證中心地址: http://xxlssoserver.com:8080/xxl-sso-server

二、Client01應用地址: http://xxlssoclient1.com:8084/xxl-sso-token-sample-springboot/

三、Client02應用地址: http://xxlssoclient2.com:8085/xxl-sso-token-sample-springboot/

啓動:xxl-sso-web-sample-springboot

配置文件信息

### xxl-sso
xxl.sso.server=http://xxlssoserver.com:8080/xxl-sso-server
xxl.sso.logout.path=/logout
xxl-sso.excluded.paths=
xxl.sso.redis.address=redis://127.0.0.1:6379

說明客戶端會重定向到認證受權中心進行登陸

redis也是鏈接同一個redis

爲何客戶端也要集成redis呢?後面源碼分析就知道了

啓動客戶端兩次,分別改成不一樣端口,模擬,發現修改配置文件,項目自動重啓,因此注意要刪除熱部署的jar包

<!-- devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

1.訪問這個客戶端,會自動重定向到server端進行登陸

2.點擊登陸,server端又跳轉到client後面帶sessionId參數

3.再訪問第二個客戶端,發現能夠免登陸

重點:斷點調試XxlSsoWebFilter源碼

思考問題:

訪問客戶端的時候,如何自動重定向到認證受權中心server端實現登陸的?

過濾器,過濾請求,若是當前沒有獲取到用戶的會話信息,會自動重定向跳轉到認證受權中心進行登陸。

因此斷點調試 核心依賴jar包中的XxlSsoWebFilter

因此找到這個類在doFilter中斷點調試

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        // make url
        String servletPath = req.getServletPath();

        // 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;
                }

            }
        }

訪問:http://xxlssoclient1.com:8084/xxl-sso-web-sample-springboot/

client原理分析:

1.先從Cookie中獲取當前的CooikeId

2.若是用戶沒有登陸的狀況下,重定向到認證受權中心進行登陸

3.在認證受權中心進行登陸成功以後返回原來地址(重定向地址)

http://xxlssoserver.com:8080/xxl-sso-server/login?redirect_url=http://xxlssoclient1.com:8084/xxl-sso-web-sample-springboot/

重點:重定向到認證受權中心源碼分析server端

在WebController中打斷點

@RequestMapping(Conf.SSO_LOGIN)
    public String login(Model model, HttpServletRequest request, HttpServletResponse response) {

        // login check
        XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response);

        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:/";
            }
        }

// login check
XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response);

跳轉到登陸界面

F12,查看提交表單url

 

因此在這裏打斷點

@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";
        }

        // 一、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.setExpireMinite(SsoLoginStore.getRedisExpireMinite());
        xxlUser.setExpireFreshTime(System.currentTimeMillis());


        // 二、make session id
        String sessionId = SsoSessionIdHelper.makeSessionId(xxlUser);

        // 三、login, store storeKey + cookie sessionId
        SsoWebLoginHelper.login(response, sessionId, xxlUser, ifRem);

        // 四、return, redirect 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.");
        }

經過用戶信息建立sessionId

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

登陸時

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);
    }

key爲sessionId,value爲用戶信息在redis中存一份

public static void put(String storeKey, XxlSsoUser xxlUser) {
        String redisKey = redisKey(storeKey);
        JedisUtil.setObjectValue(redisKey, xxlUser, redisExpireMinite * 60);  // minite to second
    }

    private static String redisKey(String sessionId){
        return Conf.SSO_SESSIONID.concat("#").concat(sessionId);
    }

認證受權登陸成功以後,在認證受權系統域名下(server端)存放對應的cookie信息

認證受權系統回調到子系統中傳遞xxl-ssso-sessionid,子系統域名下尚未對應的cookie信息

回調到子系統的時候,會被xxlssoFilter攔截

cookie信息會在客戶端域名下存一份,這樣能夠保證認證受權系統和子系統雙方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;
    }

在redis中查詢對應的sessionId信息,因此前面爲何client端也要集成redis的緣由解決了。

public static XxlSsoUser loginCheck(String  sessionId){

        String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId);
        if (storeKey == null) {
            return null;
        }

        XxlSsoUser xxlUser = SsoLoginStore.get(storeKey);
        if (xxlUser != null) {
            String version = SsoSessionIdHelper.parseVersion(sessionId);
            if (xxlUser.getVersion().equals(version)) {

                // After the expiration time has passed half, Auto refresh
                if ((System.currentTimeMillis() - xxlUser.getExpireFreshTime()) > xxlUser.getExpireMinite()/2) {
                    xxlUser.setExpireFreshTime(System.currentTimeMillis());
                    //在redis裏面也存一份
                    SsoLoginStore.put(storeKey, xxlUser);
                }

                return xxlUser;
            }
        }
        return null;
    }
public static void put(String storeKey, XxlSsoUser xxlUser) {
        String redisKey = redisKey(storeKey);
        JedisUtil.setObjectValue(redisKey, xxlUser, redisExpireMinite * 60);  // minite to second
    }

    private static String redisKey(String sessionId){
        return Conf.SSO_SESSIONID.concat("#").concat(sessionId);
    }

登陸成功

其餘系統如何實現面密登錄源碼分析

在第一個ssoclient系統在ssoserver端登陸了以後,第二個ssoclient系統登陸的話,會重定向到認證受權系統進行登陸,由於認證受權系統有對應的Cookie信息,會直接把認證受權中心第一個ssoclient登陸的cookie信息回調給第二個ssoclient系統。

訪問:http://xxlssoclient1.com:8085/xxl-sso-web-sample-springboot

也會走filter攔截,把當前會話信息也會保存在本地一份

直接免登陸

登陸流程總結

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

總體架構圖

版權@須臾之餘https://my.oschina.net/u/3995125

本文參考螞蟻課堂:http://www.mayikt.com

相關文章
相關標籤/搜索