一. 前言
記得上一篇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 |