Spring Security OAuth2.0前后端分離下的登錄授權(quán)
點擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時間送達(dá)
? 作者?|? 狂盜一枝梅
來源 |? urlify.cn/67zUBr
本篇文章將會解決上一篇文章《Spring Security OAuth2.0認(rèn)證授權(quán)五:用戶信息擴(kuò)展到j(luò)wt 》中遺留的問題,并在原有的項目中新增模塊business-server用來充當(dāng)前端頁面的web容器并轉(zhuǎn)發(fā)登錄請求和更換token的請求等,以模擬前后端分離下的登錄以及更換token操作。
一、jwt令牌在網(wǎng)關(guān)處的過期時間校驗
上一篇文章中講了在網(wǎng)關(guān)處解析token并轉(zhuǎn)發(fā)到目標(biāo)服務(wù)的操作,因為使用了jwt令牌的原因,所以省了一步到認(rèn)證服務(wù)器認(rèn)證的操作,只要驗簽成功,就認(rèn)為令牌有效。這實際上留下了一個bug:服務(wù)端無法主動取消jwt令牌,所以這個令牌只要客戶端保存下來,如果不調(diào)用認(rèn)證服務(wù)器的令牌驗證接口,這個jwt令牌將永遠(yuǎn)有效。因此需要在網(wǎng)關(guān)處加上對過期時間的校驗。
在TokenFilter中添加以下代碼邏輯
//取出exp字段,判斷token是否已經(jīng)過期
try?{
????Map?map?=?objectMapper.readValue(payLoad,?new?TypeReference 二、refresh-token接口缺少用戶信息
refresh-token在access_token過期,但是refresh-token未過期的時候使用,目的是使用refresh_token更新已經(jīng)過期的access_token,這樣理論上來說,客戶端只要能在refresh_token過期之前進(jìn)行任意操作,就可以避免重新登錄了。
上一篇文章中將用戶信息放到了jwt token中并返回給客戶端,但是如果使用refresh_token更新token,后端會報錯,前端取到的token中則缺少了用戶信息。究其原因,和JwtAccessTokenConverter有關(guān)系,關(guān)于這個類的實例,當(dāng)初創(chuàng)建的方法如下
@Bean
public?JwtAccessTokenConverter?accessTokenConverter(){
????JwtAccessTokenConverter?jwtAccessTokenConverter?=?new?JwtAccessTokenConverter();
????jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//對稱秘鑰,資源服務(wù)器使用該秘鑰來驗證
????return?jwtAccessTokenConverter;
}
這里的new操作省了很多默認(rèn)參數(shù)的指定,且先看下為啥會缺少用戶信息,擴(kuò)展用戶信息的關(guān)鍵在于方法com.kdyzm.spring.security.auth.center.service.MyUserDetailsServiceImpl#loadUserByUsername,這里擴(kuò)展了用戶信息,使其從單純的username字符串變成了UserDetailsExpand對象,然后在增強方法com.kdyzm.spring.security.auth.center.enhancer.CustomTokenEnhancer#enhance中將擴(kuò)展信息取出來放到Token中。
經(jīng)過debug,發(fā)現(xiàn)

最終發(fā)現(xiàn)是如下代碼的問題org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter#extractAuthentication
public?Authentication?extractAuthentication(Map?map)?{
????if?(map.containsKey(USERNAME))?{
????????Object?principal?=?map.get(USERNAME);
????????Collection?extends?GrantedAuthority>?authorities?=?getAuthorities(map);
????????//運行到這里的時候userDetailsService為空,所以并沒有執(zhí)行自定義的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;
}
層層網(wǎng)上追尋調(diào)用鏈,竟然是JwtAccessTokenConverter創(chuàng)建的時候省略參數(shù)導(dǎo)致的,只需要如此做就可以解決問題了
@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);//對稱秘鑰,資源服務(wù)器使用該秘鑰來驗證
????return?jwtAccessTokenConverter;
}
JwtAccessTokenConverter對象創(chuàng)建的時候指定DefaultUserAuthenticationConverter使用的userDetailsService即可。
三、新建business-server模塊作為web容器
這里新建的business-server模塊有兩個功能
充當(dāng)web容器,該服務(wù)并沒有使用模板化技術(shù),使用的是純html、css實現(xiàn)前端
轉(zhuǎn)發(fā)前端登錄、更換token請求
可能會有人對第二條有疑問,為什么要這么做?之前測試的時候基本上都是使用postman發(fā)起的請求,請求的方式是這樣的http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123可以看到這里傳遞了很重要的參數(shù)client_id和client_secret,這兩個參數(shù)無論如何也不應(yīng)當(dāng)泄露給前端,通常都是中間的真正的客戶端服務(wù)拼接這兩個參數(shù)再將請求轉(zhuǎn)發(fā)給認(rèn)證服務(wù)
四、前后端分離
設(shè)計上想要實現(xiàn)以下功能
首頁未登錄則提示用戶登錄,已經(jīng)登錄則展示用戶個人信息
用戶登錄之后將令牌保存到localStorage
token過期之后用戶可以選擇使用refresh_token更換已經(jīng)過期的令牌(access_token)
已經(jīng)過期的refresh_token不能用于更換新的令牌
1、關(guān)閉認(rèn)證服務(wù)表單登錄
以前請求認(rèn)證服務(wù)的任意接口,如果沒有認(rèn)證,則都會跳轉(zhuǎn)到系統(tǒng)自帶的登錄頁面,現(xiàn)在我們想要實現(xiàn)前后端分離了,原來系統(tǒng)自帶的登錄頁面就有些礙眼了,直接關(guān)閉就好。關(guān)閉方法如下,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
其它不做贅述,不過前端頁面寫起來挺麻煩的。。難是不難的
五、測試
源代碼:
測試前首先需要重新執(zhí)行初始化sql(auth-server/docs/sql/init.sql),然后依次啟動register-server、gateway-server、auth-server、resource-server、business-server?五個服務(wù)
啟動成功后打開瀏覽器,輸入http://127.0.0.1:30002/地址,就會看到以下頁面

點擊登錄之后,出現(xiàn)登錄框

輸入賬號密碼之后,登錄成功之后會跳轉(zhuǎn)首頁,就會看到個人信息

這里設(shè)置的token有效期為10秒,所以很快token就會失效,十秒鐘之后刷新頁面就會有新的提示

接下來可以有兩種選擇,一種是使用refresh-token更新失效的令牌,另外一種是重新登錄,這里refresh_token的有效期也很短,只有30秒,如果超出30秒,則會更新失敗,提示如下

而如果在30秒內(nèi)刷新令牌,則會重新獲取到令牌并刷新當(dāng)前頁
六、源代碼地址
源代碼地址:https://gitee.com/kdyzm/spring-security-oauth-study/tree/v7.0.0
粉絲福利:Java從入門到入土學(xué)習(xí)路線圖
??????

??長按上方微信二維碼?2 秒
感謝點贊支持下哈?
