前言
只有光頭才能變強。
文字已收錄至我的GitHub倉庫,歡迎Star:github.com/ZhongFuChen…
在我實習之前我就已經在看單點登入的是什麼了,但是實習的時候一直在忙其他的事,所以有幾個網站就一直躺在我的收藏夾裡邊:
在前陣子有個讀者來我這投稿,是使用JWT實現單點登入的(但是文章中並沒有介紹什麼是單點登入),所以我覺得是時候來整理一下了。
一、什麼是單點登入?
單點登入的英文名叫做:Single Sign On(簡稱SSO)。
在初學/以前的時候,一般我們就單系統,所有的功能都在同一個系統上。
後來,我們為了合理利用資源和降低耦合性,於是把單系統拆分成多個子系統。
- 回顧:分散式基礎知識
比如阿里系的淘寶和天貓,很明顯地我們可以知道這是兩個系統,但是你在使用的時候,登入了天貓,淘寶也會自動登入。
簡單來說,單點登入就是在多個系統中,使用者只需一次登入,各個系統即可感知該使用者已經登入。
二、回顧單系統登入
在我初學JavaWeb的時候,登入和註冊是我做得最多的一個功能了(初學Servlet的時候做過、學SpringMVC的時候做過、跟著做專案的時候做過…),反正我也數不清我做了多少次登入和註冊的功能了...這裡簡單講述一下我們初學時是怎麼做登入功能的。
眾所周知,HTTP是無狀態的協議,這意味著伺服器無法確認使用者的資訊。於是乎,W3C就提出了:給每一個使用者都發一個通行證,無論誰訪問的時候都需要攜帶通行證,這樣伺服器就可以從通行證上確認使用者的資訊。通行證就是Cookie。
如果說Cookie是檢查使用者身上的”通行證“來確認使用者的身份,那麼Session就是通過檢查伺服器上的”客戶明細表“來確認使用者的身份的。Session相當於在伺服器中建立了一份“客戶明細表”。
HTTP協議是無狀態的,Session不能依據HTTP連線來判斷是否為同一個使用者。於是乎:伺服器向使用者瀏覽器傳送了一個名為JESSIONID的Cookie,它的值是Session的id值。其實Session是依據Cookie來識別是否是同一個使用者。
所以,一般我們單系統實現登入會這樣做:
- 登入:將使用者資訊儲存在Session物件中
- 如果在Session物件中能查到,說明已經登入
- 如果在Session物件中查不到,說明沒登入(或者已經退出了登入)
- 登出(退出登入):從Session中刪除使用者的資訊
- 記住我(關閉掉瀏覽器後,重新開啟瀏覽器還能保持登入狀態):配合Cookie來用
我之前Demo的程式碼,可以參考一下:
/**
* 使用者登陸
*/
@PostMapping(value = "/user/session", produces = {"application/json;charset=UTF-8"})
public Result login(String mobileNo, String password, String inputCaptcha, HttpSession session, HttpServletResponse response) {
//判斷驗證碼是否正確
if (WebUtils.validateCaptcha(inputCaptcha, "captcha", session)) {
//判斷有沒有該使用者
User user = userService.userLogin(mobileNo, password);
if (user != null) {
/*設定自動登陸,一個星期. 將token儲存在資料庫中*/
String loginToken = WebUtils.md5(new Date().toString() + session.getId());
user.setLoginToken(loginToken);
User user1 = userService.userUpload(user);
session.setAttribute("user", user1);
CookieUtil.addCookie(response,"loginToken",loginToken,604800);
return ResultUtil.success(user1);
} else {
return ResultUtil.error(ResultEnum.LOGIN_ERROR);
}
} else {
return ResultUtil.error(ResultEnum.CAPTCHA_ERROR);
}
}
/**
* 使用者退出
*/
@DeleteMapping(value = "/session", produces = {"application/json;charset=UTF-8"})
public Result logout(HttpSession session,HttpServletRequest request,HttpServletResponse response ) {
//刪除session和cookie
session.removeAttribute("user");
CookieUtil.clearCookie(request, response, "loginToken");
return ResultUtil.success();
}
/**
* @author ozc
* @version 1.0
* <p>
* 攔截器;實現自動登陸功能
*/
public class UserInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
User sessionUser = (User) request.getSession().getAttribute("user");
// 已經登陸了,放行
if (sessionUser != null) {
return true;
} else {
//得到帶過來cookie是否存在
String loginToken = CookieUtil.findCookieByName(request, "loginToken");
if (StringUtils.isNotBlank(loginToken)) {
//到資料庫查詢有沒有該Cookie
User user = userService.findUserByLoginToken(loginToken);
if (user != null) {
request.getSession().setAttribute("user", user);
return true;
} else {
//沒有該Cookie與之對應的使用者(Cookie不匹配)
CookieUtil.clearCookie(request, response, "loginToken");
return false;
}
} else {
//沒有cookie、也沒有登陸。是index請求獲取使用者資訊,可以放行
if (request.getRequestURI().contains("session")) {
return true;
}
//沒有cookie憑證
response.sendRedirect("/login.html");
return false;
}
}
}
}
複製程式碼
總結一下上面程式碼的思路:
- 使用者登入時,驗證使用者的賬戶和密碼
- 生成一個Token儲存在資料庫中,將Token寫到Cookie中
- 將使用者資料儲存在Session中
- 請求時都會帶上Cookie,檢查有沒有登入,如果已經登入則放行
如果沒看懂的同學,建議回顧Session和Cookie和HTTP:
三、多系統登入的問題與解決
3.1 Session不共享問題
單系統登入功能主要是用Session儲存使用者資訊來實現的,但我們清楚的是:多系統即可能有多個Tomcat,而Session是依賴當前系統的Tomcat,所以系統A的Session和系統B的Session是不共享的。
解決系統之間Session不共享問題有一下幾種方案:
- Tomcat叢集Session全域性複製(叢集內每個tomcat的session完全同步)【會影響叢集的效能呢,不建議】
- 根據請求的IP進行Hash對映到對應的機器上(這就相當於請求的IP一直會訪問同一個伺服器)【如果伺服器當機了,會丟失了一大部分Session的資料,不建議】
- 把Session資料放在Redis中(使用Redis模擬Session)【建議】
- 如果還不瞭解Redis的同學,建議移步(Redis合集)
我們可以將登入功能單獨抽取出來,做成一個子系統。
SSO(登入系統)的邏輯如下:
// 登入功能(SSO單獨的服務)
@Override
public TaotaoResult login(String username, String password) throws Exception {
//根據使用者名稱查詢使用者資訊
TbUserExample example = new TbUserExample();
Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(username);
List<TbUser> list = userMapper.selectByExample(example);
if (null == list || list.isEmpty()) {
return TaotaoResult.build(400, "使用者不存在");
}
//核對密碼
TbUser user = list.get(0);
if (!DigestUtils.md5DigestAsHex(password.getBytes()).equals(user.getPassword())) {
return TaotaoResult.build(400, "密碼錯誤");
}
//登入成功,把使用者資訊寫入redis
//生成一個使用者token
String token = UUID.randomUUID().toString();
jedisCluster.set(USER_TOKEN_KEY + ":" + token, JsonUtils.objectToJson(user));
//設定session過期時間
jedisCluster.expire(USER_TOKEN_KEY + ":" + token, SESSION_EXPIRE_TIME);
return TaotaoResult.ok(token);
}
複製程式碼
其他子系統登入時,請求SSO(登入系統)進行登入,將返回的token寫到Cookie中,下次訪問時則把Cookie帶上:
public TaotaoResult login(String username, String password,
HttpServletRequest request, HttpServletResponse response) {
//請求引數
Map<String, String> param = new HashMap<>();
param.put("username", username);
param.put("password", password);
//登入處理
String stringResult = HttpClientUtil.doPost(REGISTER_USER_URL + USER_LOGIN_URL, param);
TaotaoResult result = TaotaoResult.format(stringResult);
//登入出錯
if (result.getStatus() != 200) {
return result;
}
//登入成功後把取token資訊,並寫入cookie
String token = (String) result.getData();
//寫入cookie
CookieUtils.setCookie(request, response, "TT_TOKEN", token);
//返回成功
return result;
}
複製程式碼
總結:
- SSO系統生成一個token,並將使用者資訊存到Redis中,並設定過期時間
- 其他系統請求SSO系統進行登入,得到SSO返回的token,寫到Cookie中
- 每次請求時,Cookie都會帶上,攔截器得到token,判斷是否已經登入
到這裡,其實我們會發現其實就兩個變化:
- 將登陸功能抽取為一個系統(SSO),其他系統請求SSO進行登入
- 本來將使用者資訊存到Session,現在將使用者資訊存到Redis
3.2 Cookie跨域的問題
上面我們解決了Session不能共享的問題,但其實還有另一個問題。Cookie是不能跨域的
比如說,我們請求<https://www.google.com/>
時,瀏覽器會自動把google.com
的Cookie帶過去給google
的伺服器,而不會把<https://www.baidu.com/>
的Cookie帶過去給google
的伺服器。
這就意味著,由於域名不同,使用者向系統A登入後,系統A返回給瀏覽器的Cookie,使用者再請求系統B的時候不會將系統A的Cookie帶過去。
針對Cookie存在跨域問題,有幾種解決方案:
- 服務端將Cookie寫到客戶端後,客戶端對Cookie進行解析,將Token解析出來,此後請求都把這個Token帶上就行了
- 多個域名共享Cookie,在寫到客戶端的時候設定Cookie的domain。
- 將Token儲存在SessionStroage中(不依賴Cookie就沒有跨域的問題了)
到這裡,我們已經可以實現單點登入了。
3.3 CAS原理
說到單點登入,就肯定會見到這個名詞:CAS (Central Authentication Service),下面說說CAS是怎麼搞的。
如果已經將登入單獨抽取成系統出來,我們還能這樣玩。現在我們有兩個系統,分別是www.java3y.com
和www.java4y.com
,一個SSOwww.sso.com
首先,使用者想要訪問系統Awww.java3y.com
受限的資源(比如說購物車功能,購物車功能需要登入後才能訪問),系統Awww.java3y.com
發現使用者並沒有登入,於是重定向到sso認證中心,並將自己的地址作為引數。請求的地址如下:
www.sso.com?service=www.java3y.com
sso認證中心發現使用者未登入,將使用者引導至登入頁面,使用者進行輸入使用者名稱和密碼進行登入,使用者與認證中心建立全域性會話(生成一份Token,寫到Cookie中,儲存在瀏覽器上)
隨後,認證中心重定向回系統A,並把Token攜帶過去給系統A,重定向的地址如下:
www.java3y.com?token=xxxxxxx
接著,系統A去sso認證中心驗證這個Token是否正確,如果正確,則系統A和使用者建立區域性會話(建立Session)。到此,系統A和使用者已經是登入狀態了。
此時,使用者想要訪問系統Bwww.java4y.com
受限的資源(比如說訂單功能,訂單功能需要登入後才能訪問),系統Bwww.java4y.com
發現使用者並沒有登入,於是重定向到sso認證中心,並將自己的地址作為引數。請求的地址如下:
www.sso.com?service=www.java4y.com
注意,因為之前使用者與認證中心www.sso.com
已經建立了全域性會話(當時已經把Cookie儲存到瀏覽器上了),所以這次系統B重定向到認證中心www.sso.com
是可以帶上Cookie的。
認證中心根據帶過來的Cookie發現已經與使用者建立了全域性會話了,認證中心重定向回系統B,並把Token攜帶過去給系統B,重定向的地址如下:
www.java4y.com?token=xxxxxxx
接著,系統B去sso認證中心驗證這個Token是否正確,如果正確,則系統B和使用者建立區域性會話(建立Session)。到此,系統B和使用者已經是登入狀態了。
看到這裡,其實SSO認證中心就類似一個中轉站。
參考資料:
最後
樂於輸出乾貨的Java技術公眾號:Java3y。公眾號內有200多篇原創技術文章、海量視訊資源、精美腦圖,關注即可獲取!
覺得我的文章寫得不錯,點贊!