歷史文章
Spring Security OAuth2.0認證授權一:框架搭建和認證測試
Spring Security OAuth2.0認證授權二:搭建資源服務
Spring Security OAuth2.0認證授權三:使用JWT令牌
Spring Security OAuth2.0認證授權四:分布式系統認證授權
Spring Security OAuth2.0認證授權五:用戶信息擴展到jwt
本篇文章將會解決上一篇文章《Spring Security OAuth2.0認證授權五:用戶信息擴展到jwt 》中遺留的問題,並在原有的項目中新增模塊business-server
用來充當前端頁面的web容器並轉發登錄請求和更換token的請求等,以模擬前后端分離下的登錄以及更換token操作。
一、jwt令牌在網關處的過期時間校驗
上一篇文章中講了在網關處解析token並轉發到目標服務的操作,因為使用了jwt令牌的原因,所以省了一步到認證服務器認證的操作,只要驗簽成功,就認為令牌有效。這實際上留下了一個bug:服務端無法主動取消jwt令牌,所以這個令牌只要客戶端保存下來,如果不調用認證服務器的令牌驗證接口,這個jwt令牌將永遠有效。因此需要在網關處加上對過期時間的校驗。
在TokenFilter中添加以下代碼邏輯
//取出exp字段,判斷token是否已經過期
try {
Map<String, Object> map = objectMapper.readValue(payLoad, new TypeReference<Map<String, Object>>() {
});
long expiration = ((Integer) map.get("exp")) * 1000L;
if (expiration < new Date().getTime()) {
return unAuthorized(exchange, "未認證的請求:token存在,但是已經失效",WrapperResult.TOKEN_EXPIRE);
}
} catch (IOException e) {
log.error("", e);
return unAuthorized(exchange, "未認證的請求:錯誤的token",null);
}
二、refresh-token接口缺少用戶信息
refresh-token在access_token過期,但是refresh-token未過期的時候使用,目的是使用refresh_token更新已經過期的access_token,這樣理論上來說,客戶端只要能在refresh_token過期之前進行任意操作,就可以避免重新登錄了。
上一篇文章中將用戶信息放到了jwt token中並返回給客戶端,但是如果使用refresh_token更新token,后端會報錯,前端取到的token中則缺少了用戶信息。究其原因,和JwtAccessTokenConverter
有關系,關於這個類的實例,當初創建的方法如下
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//對稱秘鑰,資源服務器使用該秘鑰來驗證
return jwtAccessTokenConverter;
}
這里的new
操作省了很多默認參數的指定,且先看下為啥會缺少用戶信息,擴展用戶信息的關鍵在於方法com.kdyzm.spring.security.auth.center.service.MyUserDetailsServiceImpl#loadUserByUsername
,這里擴展了用戶信息,使其從單純的username字符串變成了UserDetailsExpand
對象,然后在增強方法com.kdyzm.spring.security.auth.center.enhancer.CustomTokenEnhancer#enhance
中將擴展信息取出來放到Token中。
經過debug,發現
最終發現是如下代碼的問題org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter#extractAuthentication
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Object principal = map.get(USERNAME);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
//運行到這里的時候userDetailsService為空,所以並沒有執行自定義的loadUserByUsername方法
if (userDetailsService != null) {
UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
}
return null;
}
層層網上追尋調用鏈,竟然是JwtAccessTokenConverter
創建的時候省略參數導致的,只需要如此做就可以解決問題了
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
userTokenConverter.setUserDetailsService(userDetailsService);
tokenConverter.setUserTokenConverter(userTokenConverter);
jwtAccessTokenConverter.setAccessTokenConverter(tokenConverter);
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//對稱秘鑰,資源服務器使用該秘鑰來驗證
return jwtAccessTokenConverter;
}
JwtAccessTokenConverter對象創建的時候指定DefaultUserAuthenticationConverter使用的userDetailsService即可。
三、新建business-server模塊作為web容器
這里新建的business-server模塊有兩個功能
- 充當web容器,該服務並沒有使用模板化技術,使用的是純html、css實現前端
- 轉發前端登錄、更換token請求
可能會有人對第二條有疑問,為什么要這么做?之前測試的時候基本上都是使用postman發起的請求,請求的方式是這樣的http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123
可以看到這里傳遞了很重要的參數client_id
和client_secret
,這兩個參數無論如何也不應當泄露給前端,通常都是中間的真正的客戶端服務拼接這兩個參數再將請求轉發給認證服務
四、前后端分離
設計上想要實現以下功能
- 首頁未登錄則提示用戶登錄,已經登錄則展示用戶個人信息
- 用戶登錄之后將令牌保存到localStorage
- token過期之后用戶可以選擇使用refresh_token更換已經過期的令牌(access_token)
- 已經過期的refresh_token不能用於更換新的令牌
1、關閉認證服務表單登錄
以前請求認證服務的任意接口,如果沒有認證,則都會跳轉到系統自帶的登錄頁面,現在我們想要實現前后端分離了,原來系統自帶的登錄頁面就有些礙眼了,直接關閉就好。關閉方法如下,spring security的配置更改為如下:
.formLogin()
.disable();
2、前后端代碼
前端代碼在business-server/src/main/resources/static
目錄下,只有兩個頁面,一個首頁,一個登陸頁面
后端只有兩個接口
- 登錄接口:com.kdyzm.spring.security.oauth.study.business.server.controller.LoginController#login
- 更新token接口:com.kdyzm.spring.security.oauth.study.business.server.controller.TokenController#refreshToken
其它不做贅述,不過前端頁面寫起來挺麻煩的。。難是不難的
五、測試
源代碼:
測試前首先需要重新執行初始化sql(auth-server/docs/sql/init.sql),然后依次啟動 register-server
、gateway-server
、auth-server
、resource-server
、business-server
五個服務
啟動成功后打開瀏覽器,輸入http://127.0.0.1:30002/
地址,就會看到以下頁面
點擊登錄之后,出現登錄框
輸入賬號密碼之后,登錄成功之后會跳轉首頁,就會看到個人信息
這里設置的token有效期為10秒,所以很快token就會失效,十秒鍾之后刷新頁面就會有新的提示
接下來可以有兩種選擇,一種是使用refresh-token更新失效的令牌,另外一種是重新登錄,這里refresh_token的有效期也很短,只有30秒,如果超出30秒,則會更新失敗,提示如下
而如果在30秒內刷新令牌,則會重新獲取到令牌並刷新當前頁
六、源代碼地址
源代碼地址:https://gitee.com/kdyzm/spring-security-oauth-study/tree/v7.0.0
我的博客地址:https://blog.kdyzm.cn/