Spring Cloud實戰 | 最八篇:Spring Cloud +Spring Security OAuth2+ Axios前後端分離模式下無感刷新實現JWT續期
- 2020 年 11 月 23 日
- 筆記
- Spring Cloud實戰, SpringCloud, VUE
一. 前言
記得上一篇Spring Cloud的文章關於如何使JWT失效進行了理論結合代碼實踐的說明,想當然的以為那篇會是基於Spring Cloud統一認證架構系列的最終篇。但關於JWT另外還有一個熱議的話題是JWT續期?。
本篇就個人覺得比較好的JWT續期方案以及落地和大家分享一下,算是拋轉引玉,大家有好的方案歡迎留言哈。
後端
- Spring Cloud實戰 | 第一篇:Windows搭建Nacos服務
- Spring Cloud實戰 | 第二篇:Spring Cloud整合Nacos實現註冊中心
- Spring Cloud實戰 | 第三篇:Spring Cloud整合Nacos實現配置中心
- Spring Cloud實戰 | 第四篇:Spring Cloud整合Gateway實現API網關
- Spring Cloud實戰 | 第五篇:Spring Cloud整合OpenFeign實現微服務之間的調用
- Spring Cloud實戰 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT實現微服務統一認證授權
- Spring Cloud實戰 | 最七篇:Spring Cloud Gateway+Spring Security OAuth2集成統一認證授權平台下實現註銷使JWT失效方案
- Spring Cloud實戰 | 最八篇:Spring Cloud +Spring Security OAuth2+ Axios前後端分離模式下無感刷新實現JWT續期
管理前端
- vue-element-admin實戰 | 第一篇: 移除mock接入後台,搭建有來商城youlai-mall前後端分離管理平台
- vue-element-admin實戰 | 第二篇: 最小改動接入後台實現根據權限動態加載菜單
微信小程序
二. 方案
理論背景: 在 ++有來商城++ 微服務項目 OAuth2實現微服務的統一認證的背景下,前端調用/oauth/token接口認證,在認證成功會返回兩個令牌access_token和refresh_token,出於安全考慮access_token時效相較refresh_token短很多(access_token默認12小時,refresh_token默認30天)。當access_token過期或者將要過期時,需要拿refresh_token去刷新獲取新的access_token返回給客戶端,但是為了客戶良好的體驗需要做到無感知刷新。
方案一:
瀏覽器起一個定時輪詢任務,每次在access_token過期之前刷新。
方案二:
請求時返回access_token過期的異常時,瀏覽器發出一次使用refresh_token換取access_token的請求,獲取到新的access_token之後,重試因access_token過期而失敗的請求。
方案比較:
++第一種方案++實現簡單,但在access_token過期之前刷新,那些舊access_token依然能夠有效訪問,如果使用黑名單的方式限制這些就的access_token無疑是在浪費資源。
++第二種方案++是在access_token已經失效的情況下才去刷新便不會有上面的問題,但是它會多出來一次請求,而且實現起來考慮的問題相較下比較多,例如在token刷新階段後面來的請求如何處理,等獲取到新的access_token之後怎麼重新重試這些請求。
總結:第一種方案實現簡單;第二種方案更為嚴謹,過期續期不會造成已被刷掉的access_token還有效;總之兩者都是可行方案,本篇就第二種方案如何通過前後端的配合實現無感知刷新token實現JWT續期展開說明。
三. 實現
直接進入主題,如何通過代碼實現在access_token過期時使用refresh_token刷新續期,本篇涉及代碼基於Spring Cloud後端++youlai-mall++ 和 Vue前端 ++youlai-mall-admin++,需要的小夥伴可以下載到本地參考下,如果對你有幫助,也希望給個star,感謝~
後端
後端部分這裡唯一工作是在網關youlai-gateway鑒定access_token過期時拋出一個自定義異常提供給前端判定,如下圖所示:
小夥伴們在這裡也許會有疑問,網關這裡如何判斷JWT是否已過期?先不急,下文會說明,先看實現之後再說原理。
前端
1. OAuth2客戶端設置
設置OAuth2客戶端支持刷新模式,只有這樣才能使用refresh_token刷新換取新的access_token。以及為了方便我們測試分別設置access_token和refresh_token的過期時間,因為默認的12小時和30天我們吃不消的;除此之外,還必須滿足t(refresh_token) > 60s + t(access_token)的條件, refresh_token的時效大於access_token時效我們可以理解,那這個60s是怎麼回事,別急還是先看實現,原因下文會說明。
2. 添加刷新令牌方法
設置了支持客戶端刷新模式之後,在前端添加一個refreshToken方法,調用的接口和登錄認證是同一個接口/oauth/token,只是參數授權方式grant_type的值由password切換到refresh_token,即密碼模式切換到刷新模式,這個方法作用是在刷新token之後將新的token寫入到localStorage覆蓋舊的token。
3. 請求響應攔截添加令牌過期處理
在判斷響應結果是token過期時,執行刷新令牌方法覆蓋本地的token。
在刷新期間需做到兩點,一是避免重複刷新,二是請求重試,為了滿足以上兩點添加了兩個關鍵變量:
- refreshing—-刷新標識
在第一次access_token過期請求失敗時,調用刷新token請求時開啟此標識,標識當前正在刷新中,避免後續請求因token失效重複刷新。
- waitQueue—-請求等待隊列
當執行刷新token期間時,需要把後來的請求先緩存到等待隊列,在刷新token成功時,重新執行等待隊列的請求即可。
修改請求響應封裝request.js的代碼如下,關鍵部分使用注釋說明,完整工程 ++youlai-mall-admin++
let refreshing = false,// 正在刷新標識,避免重複刷新
waitQueue = [] // 請求等待隊列
service.interceptors.response.use(
response => {
const {code, msg, data} = response.data
if (code !== '00000') {
if (code === 'A0230') { // access_token過期 使用refresh_token刷新換取access_token
const config = response.config
if (refreshing == false) {
refreshing = true
const refreshToken = getRefreshToken()
return store.dispatch('user/refreshToken', refreshToken).then((token) => {
config.headers['Authorization'] = 'Bearer ' + token
config.baseURL = '' // 請求重試時,url已包含baseURL
waitQueue.forEach(callback => callback(token)) // 已刷新token,所有隊列中的請求重試
waitQueue = []
return service(config)
}).catch(() => { // refresh_token也過期,直接跳轉登錄頁面重新登錄
MessageBox.confirm('當前頁面已失效,請重新登錄', '確認退出', {
confirmButtonText: '重新登錄',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}).finally(() => {
refreshing = false
})
} else {
// 正在刷新token,返回未執行resolve的Promise,刷新token執行回調
return new Promise((resolve => {
waitQueue.push((token) => {
config.headers['Authorization'] = 'Bearer ' + token
config.baseURL = '' // 請求重試時,url已包含baseURL
resolve(service(config))
})
}))
}
} else {
Message({
message: msg || '系統出錯',
type: 'error',
duration: 5 * 1000
})
}
}
return {code, msg, data}
},
error => {
return Promise.reject(error)
}
)
四. 測試
完成上面前後端代碼調整之後,接下來進入測試,還記得上面設置access_token時效為1s、refresh_token為120s吧。這裡access_token設置為1s,但是時效確是61s,至於原因下文細說。這裡把測試根據時間分為3個階段:
- 0~61s:雙token都沒過期,正常請求過程。
- 61s~120s:access_token過期,再次請求會執行一次刷新請求。
- 120s+: refresh_token過期,神仙都救不了,重新登錄。
五. 問題
聲明: 問題基於++youlai-mall++項目使用的nimbus-jose-jwt這個JWT庫,依賴spring-security-oauth2-jose這個jar包。
1. 如何判定JWT過期?
JWT的是否過期判斷最終落點是在JwtTimestampValidator#validate方法上
2.為什麼access_token比設定多了60s時效?
開掛?有後台?向天再借60s?
剛開始在不知情的情況下以為自己哪裡配置錯了,設置5s過期,等個1min多鍾。後來確實沒辦法決心去調試下源碼,最後找到JWT驗證過期的方法JwtTimestampValidator#validate
基本上滿足 Instant.now(this.clock).minus(this.clockSkew).isAfter(expiry)
就說明JWT過期了
now – 60s > expiry =轉換=> now > expiry + 60s
按正常理解當前時間大於過期時間就可判定為過期,但這裡卻在過期時間加了個時鐘偏移60s,活生生的延長了一分鐘,至於為什麼?沒找到說明文檔,注釋也沒說明,知道的小夥伴歡迎下方留言~
六. 總結
本篇講述 ++youlai-mall++ 項目中如何通過前後端配合利用雙token刷新實現JWT續期的功能需求,後端拋出token過期異常,前端捕獲之後調用刷新token請求,成功則完成續期,失敗(一般指refresh_token也過期了)則需要重新登錄。在代碼的實現過程中了解到在資源服務器(youlai-gateway)如何判斷JWT是否過期、axios如何進行請求重試等一些問題。
最後說一下自己的項目,++youlai-mall++ 集成當前主流開發模式微服務加前後端分離,當前最新主流技術棧 Spring Cloud + Spring Cloud Alibaba + Vue , 以及最流行統一認證授權Spring Cloud Gateway + Spring Security OAuth2 + JWT。所以覺得本文對你有所幫助的話給個關注(持續更新中…),或者對該項目感興趣的小夥伴給個star,也期待你的加入和建議,還是老樣子有問題隨時聯繫我~(微信號:haoxianrui)。
項目名稱 | 地址 |
---|---|
後台 | youlai-mall |
管理前端 | youlai-mall-admin |
微信小程序 | youlai-mall-weapp |