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

最終發(fā)現(xiàn)是如下代碼的問(wè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);
????????//運(yùn)行到這里的時(shí)候userDetailsService為空,所以并沒(méi)有執(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í)候省略參數(shù)導(dǎo)致的,只需要如此做就可以解決問(wèn)題了
@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);//對(duì)稱秘鑰,資源服務(wù)器使用該秘鑰來(lái)驗(yàn)證
????return?jwtAccessTokenConverter;
}
JwtAccessTokenConverter對(duì)象創(chuàng)建的時(shí)候指定DefaultUserAuthenticationConverter使用的userDetailsService即可。
三、新建business-server模塊作為web容器
這里新建的business-server模塊有兩個(gè)功能
充當(dāng)web容器,該服務(wù)并沒(méi)有使用模板化技術(shù),使用的是純html、css實(shí)現(xiàn)前端
轉(zhuǎn)發(fā)前端登錄、更換token請(qǐng)求
可能會(huì)有人對(duì)第二條有疑問(wèn),為什么要這么做?之前測(cè)試的時(shí)候基本上都是使用postman發(fā)起的請(qǐng)求,請(qǐng)求的方式是這樣的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,這兩個(gè)參數(shù)無(wú)論如何也不應(yīng)當(dāng)泄露給前端,通常都是中間的真正的客戶端服務(wù)拼接這兩個(gè)參數(shù)再將請(qǐng)求轉(zhuǎn)發(fā)給認(rèn)證服務(wù)
四、前后端分離
設(shè)計(jì)上想要實(shí)現(xiàn)以下功能
首頁(yè)未登錄則提示用戶登錄,已經(jīng)登錄則展示用戶個(gè)人信息
用戶登錄之后將令牌保存到localStorage
token過(guò)期之后用戶可以選擇使用refresh_token更換已經(jīng)過(guò)期的令牌(access_token)
已經(jīng)過(guò)期的refresh_token不能用于更換新的令牌
1、關(guān)閉認(rèn)證服務(wù)表單登錄
以前請(qǐng)求認(rèn)證服務(wù)的任意接口,如果沒(méi)有認(rèn)證,則都會(huì)跳轉(zhuǎn)到系統(tǒng)自帶的登錄頁(yè)面,現(xiàn)在我們想要實(shí)現(xiàn)前后端分離了,原來(lái)系統(tǒng)自帶的登錄頁(yè)面就有些礙眼了,直接關(guān)閉就好。關(guān)閉方法如下,spring security的配置更改為如下:
????????????????.formLogin()
????????????????.disable();2、前后端代碼
前端代碼在business-server/src/main/resources/static目錄下,只有兩個(gè)頁(yè)面,一個(gè)首頁(yè),一個(gè)登陸頁(yè)面
后端只有兩個(gè)接口
登錄接口:com.kdyzm.spring.security.oauth.study.business.server.controller.LoginController#login
更新token接口:com.kdyzm.spring.security.oauth.study.business.server.controller.TokenController#refreshToken
其它不做贅述,不過(guò)前端頁(yè)面寫起來(lái)挺麻煩的。。難是不難的
五、測(cè)試
源代碼:
測(cè)試前首先需要重新執(zhí)行初始化sql(auth-server/docs/sql/init.sql),然后依次啟動(dòng)register-server、gateway-server、auth-server、resource-server、business-server?五個(gè)服務(wù)
啟動(dòng)成功后打開(kāi)瀏覽器,輸入http://127.0.0.1:30002/地址,就會(huì)看到以下頁(yè)面

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

輸入賬號(hào)密碼之后,登錄成功之后會(huì)跳轉(zhuǎn)首頁(yè),就會(huì)看到個(gè)人信息

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

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

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

??長(zhǎng)按上方微信二維碼?2 秒
感謝點(diǎn)贊支持下哈?
