版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 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.com,b.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.com,b.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的全局會話,這樣做不到用戶無感知,加一個方法就是爲了不做頁面跳轉。