java實現 SSO 單點登錄(最終版)--補充完全跨域SSO

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/zhangjingao/article/details/89052764

前言

  前面我寫了一篇文章,java實現完全跨域SSO單點登錄,最後我會比較兩種方案。
  那篇文章主要說明完全跨域SSO單點登錄的實現,但是我最終並沒有使用那篇,當然,那篇完全可以實現SSO跨域,但是那篇有一些不太優雅的地方,我綜合我的場景等各方面考慮,最終選擇了我下面的這個方案。因爲那篇並沒有被選用,所以代碼大家可以隨意看,但是下面這個方案因爲代碼已經在使用,所以不太方便分享代碼,見諒。大家有什麼問題,歡迎留言或者qq私我,如果是qq(1763608200)私我,建議加驗證信息:sso探討(內心os: 不加驗證信息我不會通過的,最近信息泄露太嚴重了,好多頭像是雜七雜八的不知道是賣茶還是幹啥的老加我

同父域單點登錄

什麼是SSO

  SSO(Single Sign On)單點登錄是實現多個系統之間統一登錄的驗證系統,簡單來說就是:有A,B,C三個系統,在A處登錄過後,再訪問B系統,B系統就已經處於了登錄狀態,C系統也是一樣。舉個生活中栗子:你同時打開天貓和淘寶,都進入login界面,都要求你登錄的,現在你在淘寶處登錄後,直接在天貓處刷新,你會發現,你已經登錄了,而且就是你在淘寶上登錄的用戶。說明他們實現了SSO,並且持有相同的信息。
  當然這個特性意味着它的使用場景是:同一公司下的不同子系統,因爲對於SSO來說,每一個子系統擁有的信息都一樣,是用戶的全部信息,如果是不同公司,那這肯定不合適。

這套方案我們要實現的效果

  假設此時有A,B,C三套系統,他們有相同的頂級域名,就是說他們是同一個域名的子域名,此處使用a.xxx.comb.xxx.com,c.xxx.com來表示,有一個獨立的驗證中心,使用sso.xxx.com表示。當我們在a.xxx.com下登錄後,我當前的瀏覽器訪問b.xxx.com依然能直接訪問,並且已經處於登錄狀態。當我在A系統中註銷登錄時,B系統也會被註銷登錄。但是如果是b.yyy.com在該方案下就不支持了。

具體流程

在這裏插入圖片描述
  初始A,B,C全部處於未登錄狀態,沒有令牌(tokenid)
  1、用戶訪問A系統,沒有令牌,A系統驗證未登錄,直接返回A的登錄界面
  2、A輸入賬號密碼進行登錄請求到SSO中(附帶重定向url)
  3、然後SSO驗證用戶信息正確,生成身份令牌,將身份令牌轉爲jwt,將用戶信息使用AES加密,將jwt身份令牌和用戶信息作爲鍵值對存進redis,時間設爲30分鐘,將身份令牌種進cookie,重定向回攜帶的url(到達了子系統)
  4、子系統發現有身份令牌,發送請求到達SSO驗證令牌真僞
  5、SSO收到驗證請求,驗證通過,返回AES加密的用戶信息
  6、子系統收到通過信息,將用戶信息解密,存進session一分鐘(token),跳轉頁面至首頁或用戶請求頁
  7、一分鐘失效後或者子系統訪問時,發現有令牌但session中無信息,再次重複4,5,6

解釋幾個疑問點

1、sso中redis存儲的是全局會話,而每個子系統中session存儲的是臨時會話
2、爲什麼臨時會話定義一分鐘
  定義一分鐘,就是爲了當A系統退出後,註銷了本地臨時會話,再到sso註銷全局會話,一分鐘的話即使不去註銷B系統的臨時會話,也很快就會消失,這個時間可以忽略,再者一分鐘可以保證子系統和sso實時保證用戶信息的統一,假設此時該用戶修改了個人信息,其他系統也能保證儘快得到最新的信息。
3、爲什麼sso中要用redis存儲身份令牌呢?爲什麼不用session呢
  首先各個子系統驗證身份令牌時是由各個子系統發出的,不一定是登錄的那個域名了,所以無法保證session還能跟登錄的那個session一致,所以使用redis保存,這樣身份令牌和sso的redis之間也模擬出來一個全局會話關係出來。
4、爲什麼要用JWT呢
  是爲了實現數字簽名,如果黑客竊取到身份令牌,暴力枚舉令牌,是有機會泄露的,使用JWT後,sso可以驗證這是否被篡改。
5、爲什麼要重定向回子系統
  首先A系統登錄時是在自己的登錄界面,然後直接提交到sso驗證中心,這個時候這個請求是瀏覽器和sso建立的聯繫,跟子系統沒關係,直接返回或者轉發什麼的是沒辦法回到子系統的。
6、爲什麼登錄要直接提交到SSO驗證中心?(內心os:我感覺我快成了槓精,可能我適合去工地,2333
  因爲身份令牌要保存在瀏覽器客戶端,保存在cookie中,也就是說sso此時要拿到瀏覽器請求才能給瀏覽器的父域種下cookie,如果是通過子系統再發出請求,那樣是種不了cookie的,當然如果是登錄請求提交到子系統,子系統發出驗證,由子系統去種cookie也是可以的,但是這樣的話你內聚就降低了很多,耦合度也高了,子系統原本不用管的登錄的事也要管了,sso原本要全管的登錄被別人幹了一部分。

簡單看下代碼

SSO的統一登錄代碼

 
   /**
     * 統一驗證登錄中心
     * @param username 用戶名
     * @param password 密碼
     */
    @PostMapping
    public void checkLogin(String username, String password, String returnUrl,HttpServletResponse response) {
        TbUser user = userService.login(username, password);
        log.info("user: "+user);
        String jwtValue = null;
        if (user != null) {
            /*
             * 獲得uuid,作爲tokenId(TGC)
             * 將tokenId存進jedis,返回客戶端以jwt存儲的tokenId,
             * 即使tokenId被截取也無所謂
             */
            String tokenId = UUID.randomUUID().toString();
            //生成jwt
            jwtValue = new JwtUtil(tokenId, null).creatJwt();
            log.info("tokenId: " + tokenId+" jwt: "+jwtValue);
            if (jwtValue != null) {
                Jedis jedis = jedisPool.getResource();
                jedis.set(tokenId, user.toString());
                jedis.expire(tokenId,1800);
                log.info("查看key的剩餘生存時間:"+jedis.ttl(tokenId));
            }
        }
        //將jwt加密的TGC存進cookie
        Cookie cookie = new Cookie("tokenId", jwtValue);
        cookie.setPath("/"); //設置根域名
        cookie.setHttpOnly(true);
        response.addCookie(cookie);
        try {
            response.sendRedirect(returnUrl);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

子系統使用身份令牌驗證


    /**
     * 驗證wlId
     * 用jedis的原因是不用的話無法從所有session會話中找到sessionid爲tokenid的會話
     * @param wlId wlId
     * @return  如果失效返回null,反之返回aes加密的用戶信息
     */
    @GetMapping
    public String tokenCheck (String wlId) {
        //從jedis中獲得token
        Jedis jedis = jedisPool.getResource();
        String user = null;
        if (wlId != null) {
            wlId = new JwtUtil(null, wlId).decryptJwt();
            user = jedis.get(wlId);
            jedis.expire(wlId,1800);
            log.info("tokencheck查看key的剩餘生存時間:"+jedis.ttl(wlId));
        }
        return user;
    }

退出登錄的方法


    /**
     * 清除全局會話,子系統跳轉到回到登錄頁的方法
     * @param wlId 身份令牌
     */
    @GetMapping("/loginout")
    public void loginOut(String wlId) {
        log.info("clear token");
        Jedis jedis = jedisPool.getResource();
        jedis.del(wlId);
    }


與我另一篇完全跨域sso對比

  首先附上篇文章鏈接:java實現完全跨域SSO單點登錄
  1、在以後你們的系統可能短時間內不會使用多個根域名的話,建議使用該版本或者修改該版本。
  2、兩者都可以實現單點登錄,我已經自己試驗過了。並且另一個版本有github代碼提供。
  3、上篇是完全跨域SSO,即使以後使用不同的域名,完全沒問題。該版本只適用於同父域單點登錄。
  4、上篇雖然是完全跨域sso,但是sso需要多維護一個子系統表,用來記錄向子系統添加cookie的鏈接,該篇不需要。
  5、當以後用戶量大,業務拓展的時候,第一個登錄的子系統不僅要使用更多的時間驗證用戶是否正確,並且還要花時間對那麼多個子系統進行重定向種cookie,雖然可以先跳轉到用戶首頁再去請求,但是也會造成第一個登陸的登錄速度慢一些,用戶體驗可能就不那麼好。

在這個版本上補充完全跨域SSO內容

這個補充是我後來想的,我並未實踐,但是我覺得可行,歡迎留言或加q探討

先看下草圖

在這裏插入圖片描述
有a,b兩個系統,域名爲a.comb.com。然後sso爲 sso.com。首先都處於未登錄狀態a系統和b系統都是自己獨立的登錄界面。
1、a首先去請求,然後a的server發現未登錄,這個時候首先重定向sso.checkTokenId,這樣sso發現cookie下確實沒有種下身份令牌這樣就重定向回a.login界面
2、a在登錄界面提交賬號密碼到達sso.login方法,sso驗證信息正確,那麼就爲sso.com的cookie種下身份令牌這個就是全局會話,重定向回最初的訪問鏈接,並攜帶身份令牌,在a處生成了局部會話。
3、現在客戶端再去訪問b.com下的頁面,這個時候b.server發現沒有局部會話,未登錄,這個時候首先重定向到sso.checkTokenId,sso驗證是否有全局的身份令牌
4、sso驗證有身份令牌,那麼就返回b最初的訪問鏈接,攜帶身份令牌,在b處生成局部會話,b處也就成了已登錄
5、sso驗證沒有身份令牌,那麼就重新到b.com下的b.login界面,重複2步驟,最終生成全局會話和局部會話

涉及到的問題

1、每次重定向攜帶信息,這個時候可以使用302重定向跳轉,可以實現get重定向並攜帶參數
2、sso系統加了一個checkTokenId方法就是精華,這樣每次判斷是否有會話,就可以重定向到此獲得sso.com的cookie,這樣就可以得到全局的身份令牌
3、此時各個子系統使用的是不同的登錄界面,如果想要使用統一的登錄界面,依然要採用本設計,因爲如果只是單純的靠統一的登錄界面來獲得sso.com的全局會話,這樣做不到用戶無感知,加一個方法就是爲了不做頁面跳轉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章