有關單點登入(SSO)之前有寫過兩篇文章
如果說XXL-JOB你可能並不陌生,它是非常火爆的一個分散式任務排程平臺。但其實在該作者還有一個非常優秀的開源專案叫XXL-SSO,這兩個個專案都是1000+Star。
XXL-SSO 是一個分散式單點登入框架。只需要登入一次就可以訪問所有相互信任的應用系統。 擁有"輕量級、分散式、跨域、Cookie+Token均支援、Web+APP均支援"等特性。現已開放原始碼,開箱即用。
這裡主要是透過對XXL-SSO原始碼的分析,將理論和實踐結合!
一、快速接入sso
1、xxl-sso特性
- 簡潔:API直觀簡潔,可快速上手
- 輕量級:環境依賴小,部署與接入成本較低
- 單點登入:只需要登入一次就可以訪問所有相互信任的應用系統
- 分散式:接入SSO認證中心的應用,支援分散式部署
- HA:Server端與Client端,均支援叢集部署,提高系統可用性
- 跨域:支援跨域應用接入SSO認證中心
- Cookie+Token均支援:支援基於Cookie和基於Token兩種接入方式,並均提供Sample專案
- Web+APP均支援:支援Web和APP接入
- 實時性:系統登陸、登出狀態,全部Server與Client端實時共享
- CS結構:基於CS結構,包括Server"認證中心"與Client"受保護應用"
- 記住密碼:未記住密碼時,關閉瀏覽器則登入態失效;記住密碼時,支援登入態自動延期,在自定義延期時間的基礎上,原則上可以無限延期
- 路徑排除:支援自定義多個排除路徑,支援Ant表示式,用於排除SSO客戶端不需要過濾的路徑
2、環境
- JDK:1.7+
- Redis:4.0+
3、 原始碼地址
4、 專案結構說明
- 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被禁
5、 架構圖
應用系統
:sso-web系統(8081埠)、sso-web系統(8082埠)(需要登入的系統)
SSO客戶端
:登入、退出(獨立jar包給應用系統引用)
SSO服務端
:登入(登入服務)、登入狀態(提供登入狀態校驗/登入資訊查詢的服務)、退出(使用者登出服務)
資料庫
:儲存使用者賬戶資訊(一般使用Mysql,在當前專案中為了簡便並沒有查詢資料庫)
快取
:儲存使用者的登入資訊(使用Redis)
二、快速接入XXL-SSO框架
1、 部署認證中心(sso-server)
只需要修改配置檔案即可,配置檔案位置:application.properties
## 配置redis
xxl.sso.redis.address=redis://118.31.224.65:6379
## 登入態有效期視窗,預設24H,當登入態有效期視窗過半時,自動順延一個週期
xxl.sso.redis.expire.minute=1440
2、 部署'單點登陸Client端接入示例專案'
這裡指需要接入SSO的系統,在當前專案有xxl-sso-web-sample-springboot和 xxl-sso-token-sample-springboot兩個示例專案,這裡暫且以sso-web為示例。
1)、maven依賴
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-sso-core</artifactId>
<version>${最新穩定版}</version>
</dependency>
2)配置 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.server=http://ssoserver.com:8080/xxl-sso-server
## 退出介面
xxl.sso.logout.path=/logout
## 排除走sso的介面
xxl-sso.excluded.paths=/excludedUrl
## redis地址
xxl.sso.redis.address=redis://118.11.214.65:6379
三、快速驗證
1、修改host檔案
修改Host檔案:域名方式訪問認證中心,模擬跨域與線上真實環境
127.0.0.1 ssoserver.com
127.0.0.1 webb.com
127.0.0.1 weba.com
2、啟動專案
分別執行 “xxl-sso-server” 與 “xxl-sso-web-sample-springboot”,為了驗證單點登入,這裡sso-web需求啟動兩次,只是一次是8081埠,一次是8082埠。
## 1、SSO認證中心地址:
http://ssoserver.com:8080/xxl-sso-server
## 2、Client01應用地址:
http://weba.com:8081/xxl-sso-web-sample-springboot/
## 3、Client02應用地址:
http://webb.com:8082/xxl-sso-web-sample-springboot/
3、驗證
SSO登入流程
正常情況下,登入流程如下:
1、訪問 "Client01應用地址" ,將會自動 redirect 到 "SSO認證中心地址" 登入介面
2、成功登入後,將會自動 redirect 返回到 "Client01應用地址",並切換為已登入狀態
3、此時,訪問 "Client02應用地址",不需登陸將會自動切換為已登入狀態
很明顯Client01
登入成功後,Client02無需再重新登入就可以訪問了。
SSO登出流程
正常情況下,登出流程如下:
1、訪問 "Client01應用地址" 配置的 "登出登陸path",將會自動 redirect 到 "SSO認證中心地址" 並自動登出登陸狀態
2、此時,訪問 "Client02應用地址",也將會自動登出登陸狀態
四、核心程式碼分析
1、SSO客戶端(sso-core)攔截器
主要看sso攔截器流程就可以了
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 獲取當前請求介面
String servletPath = req.getServletPath();
// 1、是否是排除 不走sso的介面 如果是直接放行
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;
}
}
}
// 2、是否是退出登入介面
if (logoutPath != null
&& logoutPath.trim().length() > 0
&& logoutPath.equals(servletPath)) {
// 2.1刪除當前請求客戶端的cookie
SsoWebLoginHelper.removeSessionIdByCookie(req, res);
// 2.2重定向到sso認證服務的 退出介面
String logoutPageUrl = ssoServer.concat(Conf.SSO_LOGOUT);
res.sendRedirect(logoutPageUrl);
return;
}
// 3、校驗使用者(是否存在,是否過期) 這個方法下面在展開
XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(req, res);
// 4、令牌校驗失敗
if (xxlUser == null) {
//獲取當前請求地址
String link = req.getRequestURL().toString();
// 重定向到sso認證服務的 登入介面
String loginPageUrl = ssoServer.concat(Conf.SSO_LOGIN)
+ "?" + Conf.REDIRECT_URL + "=" + link;
res.sendRedirect(loginPageUrl);
return;
}
// ser sso user
request.setAttribute(Conf.SSO_USER, xxlUser);
// 已經登入 放行
chain.doFilter(request, response);
return;
}
在看下上面的loginCheck方法
/**
* 令牌校驗
*
* @return 使用者資訊
*/
public static XxlSsoUser loginCheck(HttpServletRequest request, HttpServletResponse response){
//去cookie去獲取xxl_sso_sessionid 其實就是之前原理篇說的token,只是名稱叫法不同
String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID);
// 這裡去redis中獲取使用者資訊 有可能獲取不到。這個方法就不貼上了 這裡有三種情況
//1、cookieSessionId為空 那麼直接返回null
//2、cookieSessionId不為空,但在redis獲取不到使用者資訊,因為存在其它系統退出後 redis刪除了
//3、redis獲取到了使用者資訊,但超過有效期了 依舊返回null
XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(cookieSessionId);
if (xxlUser != null) {
return xxlUser;
}
// 如果獲取不到 所以已經在其它系統退出登入了 那刪除cookie中的xxl_sso_sessionid
SsoWebLoginHelper.removeSessionIdByCookie(request, response);
//如果是 sso登入成功後 回撥過來的 這個時候在這裡是可以獲取到xxl_sso_sessionid的
String paramSessionId = request.getParameter(Conf.SSO_SESSIONID);
xxlUser = SsoTokenLoginHelper.loginCheck(paramSessionId);
if (xxlUser != null) {
CookieUtil.set(response, Conf.SSO_SESSIONID, paramSessionId, false); /
return xxlUser;
}
return null;
}
2、認證伺服器(sso-server)登入介面
/**
* sso認證中心 登入介面
*/
@RequestMapping(Conf.SSO_LOGIN)
public String login(Model model, HttpServletRequest request, HttpServletResponse response) {
// 同樣的 該判斷sso上有沒有全域性會話
XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response);
//如果 其它系統登入成功過 這個就不回為null 直接再帶上xxl_sso_sessionid=xxx 重定向到之前介面
//也不用在登入了
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:/";
}
}
//只有全域性會話不存在 才會跳轉登入頁面
model.addAttribute("errorMsg", request.getParameter("errorMsg"));
model.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));
return "login";
}
3、認證伺服器(sso-server)退出介面
@RequestMapping(Conf.SSO_LOGOUT)
public String logout(HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes) {
// 退出操作
SsoWebLoginHelper.logout(request, response);
// 跳轉到登入頁
redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));
return "redirect:/login";
}
再來看下 logout 方法做了哪些事情
public static void logout(HttpServletRequest request,
HttpServletResponse response) {
String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID);
if (cookieSessionId==null) {
return;
}
//1、刪除全域性快取 redis中 清除cookieSessionId,這樣其它系統在令牌校驗的時候 會失敗。即一處退出,處處退出。
String storeKey = SsoSessionIdHelper.parseStoreKey(cookieSessionId);
if (storeKey != null) {
SsoLoginStore.remove(storeKey);
}
//2、清除全域性會話
CookieUtil.remove(request, response, Conf.SSO_SESSIONID);
}
整個核心程式碼的邏輯都在這裡了,其實結合上一篇的理論篇,理解起來就一點也不復雜了。
宣告: 公眾號如需轉載該篇文章,發表文章的頭部一定要 告知是轉至公眾號: 後端元宇宙。同時也可以問本人要markdown原稿和原圖片。其它情況一律禁止轉載!