前言
單點登錄SSO(Single Sign On),凡是有一定的開發經驗的童鞋都應該有應用或者瞭解過,小編還是實習生的時候,看到登錄某個應用服務後,再跳轉其他應用服務,竟然不用再次登錄了,覺得賊拉風,不知道大家第一見這種場景時是不是跟小編一樣的感覺。今天小編給大家介紹一款分佈式單點登錄組件xxl-sso,目的就是讓大家能短時間內快速的應用到項目中,並從中瞭解其中的相關的實現原理。
項目介紹
xxl-sso是一款基於redis輕量級分佈式高可用的SSO實現組件,支持web端(Cookie實現)和app端(Token實現)兩種方式,兩種方式的驗證都是用Filter實現的,小編之所以叫組件不叫框架,是因爲集成起來超級方便,源碼也非常易懂。廢話不多說,直接進入實戰把。
實戰
1.認證中心部署
從https://github.com/xuxueli/xxl-sso/下載項目,將xxl-sso-server單獨拷貝到自己的創建的項目中(其實直接用就可以了,小編喜歡拷貝過來以便自定義修改),這個模塊只需要修改下application.properties中的redis配置就可以了。
### web
server.port=8880
server.servlet.context-path=/xxl-sso-server
### resources
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/
### freemarker
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.request-context-attribute=request
spring.freemarker.settings.number_format=0.##########
### xxl-sso "redis://xxl-sso:[email protected]:6379/0" redis://xxl-sso:123456@localhost:6379/0
xxl.sso.redis.address=redis://127.0.0.1:6379/0
xxl.sso.redis.expire.minite=1440
這裏xxl.sso.redis.address是小編自己本地安裝的redis單機默認配置信息,還可以通過逗號分隔進行集羣,目前這組件只支持分片集羣ShardedJedisPool,組件後續會支持JedisCluster,注意redis://{username}:{password}@{ip}:{port}/{db}這種需要密碼的配置方式,密碼一定不要帶#,!,@,$等特殊符號,ShardedJedisPool初始化鏈接時會報解析錯誤。
啓動項目,效果如下
好了,認證中心OK了,小編項目太多了,端口這裏改成了8880,contextPath爲/xxl-sso-server,之所以強調端口和contextPath,是因爲應用端要用到。
注意 : 認證中心的login接口默認是查的內存,具體業務開發中需要改成讀數據庫的形式。
2.應用端配置
2.1 創建一個新的spring boot module,引入核心jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-sso-core</artifactId>
<version>1.1.0</version>
</dependency>
2.2 修改application.properties,如下
### web
server.port=8881
server.servlet.context-path=/xxl-sso-client
### xxl-sso
xxl.sso.server=http://xxlssoserver.com:8880/xxl-sso-server
xxl.sso.logout.path=/logout
xxl-sso.excluded.paths=
xxl.sso.redis.address=redis://127.0.0.1:6379
### freemarker
spring.freemarker.request-context-attribute=request
spring.freemarker.cache=false
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
### resource (default: /** + classpath:/static/ )
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/
demo用的是官方的freemarker來演示登錄成功的界面,需要需要在classpath:/templates目錄下放入一個頁面模板,官方的代碼freemarker沒有配置完整,小編是踩坑後自己補全的
重點 : xxl.sso.server改成認證中心的項目地址
xxl.sso.redis.address需要修改與認證中心同一個redis配置
2.3 創建配置類
@Configuration
public class XxlSsoConfig implements DisposableBean {
@Value("${xxl.sso.server}")
private String xxlSsoServer;
@Value("${xxl.sso.logout.path}")
private String xxlSsoLogoutPath;
@Value("${xxl.sso.redis.address}")
private String xxlSsoRedisAddress;
@Value("${xxl-sso.excluded.paths}")
private String xxlSsoExcludedPaths;
@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 XxlSsoTokenFilter()); //token驗證方式
registration.setFilter(new XxlSsoWebFilter()); //cookie驗證方式
//xxlSsoServer用來拼接sso server登錄/登出接口,進行重定向跳轉到認證中心,而且只要到登錄界面就要帶上回調地址,否則回跳到服務器內部默認頁面
registration.addInitParameter(Conf.SSO_SERVER, xxlSsoServer);
//xxlSsoLogoutPath用來指定登出的路徑,相對當前應用contextPath
registration.addInitParameter(Conf.SSO_LOGOUT_PATH, xxlSsoLogoutPath);
//xxlSsoExcludedPaths用來指定不需要校驗的路徑,相對當前應用contextPath,多個用逗號分隔,支持ANT表達式
registration.addInitParameter(Conf.SSO_EXCLUDED_PATHS, xxlSsoExcludedPaths);
return registration;
}
@Override
public void destroy() throws Exception {
// xxl-sso, redis close
JedisUtil.close();
}
}
cookie和token驗證方式的實現是可插拔的,校驗邏輯實現類爲XxlSsoWebFilter(cookie)和XxlSsoTokenFilter(token),裏面實現流程是一樣的,只是驗證的方式不一樣。
校驗流程
-
校驗排除的路徑,排除不需要校驗的訪問路徑
-
校驗contextPath後面路徑是否是logout,是logout則清除認證中心放入redis的token(userId+version憑藉的唯一標識)憑據
userId是用戶對象的唯一標識,version就是UUID
-
校驗憑據的合法性以及時效性
校驗方式
-
WEB cookie驗證方式
認證中心登錄成功後,會把token存入redis並以重定向的方式傳給應用端,應用端將token寫入瀏覽器的cookie中,web端再次請求時,應用端獲取HttpServletRequest中cookie中的token與redis中的token進行對比
-
APP token驗證方式
認證中心以登錄接口的形式提供token並存入redis,再次訪問應用端接口時,應用端會取HttpServletRequest裏面的header中key爲xxl_sso_sessionid對應的值(token)與redis中的token進行對比
具體的對比細節,有興趣的童鞋可以看下源碼或者進行留言,大家探討下。
3.效果演示
爲了模擬多個應用端跨域場景,需要修改下hosts文件,添加如下信息
127.0.0.1 xxlssoserver.com
127.0.0.1 xxlssoclient1.com
127.0.0.1 xxlssoclient2.com
認證中心 初始化幾個用戶對象到內存中,也可以改成查數據庫的形式,認證中心登錄接口會調用findUser方式
@Service
public class UserServiceImpl implements UserService {
private static List<UserInfo> mockUserList = new ArrayList<>();
static {
for (int i = 0; i <5; i++) {
UserInfo userInfo = new UserInfo();
userInfo.setUserid(1000+i);
userInfo.setUsername("user" + (i>0?String.valueOf(i):""));
userInfo.setPassword("123456");
mockUserList.add(userInfo);
}
}
/**
* 登錄時需要驗證用戶名稱和密碼,提供查詢,可以改成數據庫查詢形式
**/
@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.");
}
// mock user
for (UserInfo mockUser: mockUserList) {
if (mockUser.getUsername().equals(username) && mockUser.getPassword().equals(password)) {
return new ReturnT<UserInfo>(mockUser);
}
}
return new ReturnT<UserInfo>(ReturnT.FAIL_CODE, "username or password is invalid.");
}
}
應用端 添加一個跳轉到登錄成功頁的接口,頁面是上面提到的freemarker的index.flt,然後再添加一個測試接口
@RequestMapping("/")
public String index(Model model, HttpServletRequest request) {
XxlSsoUser xxlUser = (XxlSsoUser) request.getAttribute(Conf.SSO_USER);
model.addAttribute("xxlUser", xxlUser);
return "index";
}
@RequestMapping("/json")
@ResponseBody
public ReturnT<XxlSsoUser> json(Model model, HttpServletRequest request) {
XxlSsoUser xxlUser = (XxlSsoUser) request.getAttribute(Conf.SSO_USER);
return new ReturnT(xxlUser);
}
3.1 基於cookie
設置應用端配置FilterRegistrationBean的filter如下
registration.setFilter(new XxlSsoWebFilter()); //cookie驗證方式
- 啓動應用端後訪問地址http://xxlssoclient1.com:8881/xxl-sso-client
細心的童鞋會發現,地址被重定向了到認證中心的登錄頁,而且參數redirect_url是剛剛應用端的請求地址。
user/123456進行登錄
認證中心重定向到redirect_url的地址並帶上token(xxl_sso_sessionid)參數
- 訪問預先準備好的例子,地址http://xxlssoclient2.com:8881/xxl-sso-client/json試一下,看是否還需要登錄
直接就返回結果了,跟預期一樣免登錄了,而且回傳到應用端的token(xxl_sso_sessionid)是一樣的,而且格式正是userId_UUID
- 登出xxlssoclient1.com後看xxlssoclient2.com是否還能正常訪問
token失效了,又重新跳到了認證中心的登錄頁。
3.2 基於token
設置應用端配置FilterRegistrationBean的filter如下
registration.setFilter(new XxlSsoTokenFilter());//token驗證方式
重新啓動應用,用postman訪問認證中心的登錄接口
返回token(xxl_sso_sessionid)爲1000_0422bc223a364ec89e0ac67203032e62
- 訪問應用的測試接口http://xxlssoclient1.com:8881/xxl-sso-client/json
由於header沒填入token(xxl_sso_sessionid),報501錯誤了,加上xxl_sso_sessionid對應的值1000_0422bc223a364ec89e0ac67203032e62,再次請求
接口返回數據成功。
-
試試用這個token訪問http://xxlssoclient2.com:8881/xxl-sso-client/json
正常返回數據。
- 調用http://xxlssoclient1.com:8881/xxl-sso-client/logout後,再看http://xxlssoclient2.com:8881/xxl-sso-client/json還能訪問不
跟預期一樣,報501錯誤了,提示沒有登錄
兩種方式都演示完了,是否跟童鞋們預期的一樣呢?
總結
xxl-sso大大減少了開發與維護成本,非常適用於內部多項目集成場景,耶魯的CAS/Oauth2.0規範協議應該是目前比較常用的SSO實現,但學習成本以及配置的複雜度比xxl-sso要多很多,有興趣的童鞋可以進行對比下。
xxl-sso目前還在持續迭代中,小編使用過程中發現登入和登出後認證中心的回調地址暫時沒有參數進行配置,童鞋們可以想想怎麼實現,歡迎留言,最佳方案的送禮物。
代碼
https://github.com/pengziliu/GitHub-code-practice