Spring Cloud實戰 | 最八篇:Spring Cloud +Spring Security OAuth2+ Axios前後端分離模式下無感重新整理實現JWT續期

你好,舊時光發表於2020-11-23

一. 前言

記得上一篇Spring Cloud的文章關於如何使JWT失效進行了理論結合程式碼實踐的說明,想當然的以為那篇會是基於Spring Cloud統一認證架構系列的最終篇。但關於JWT另外還有一個熱議的話題是JWT續期?

本篇就個人覺得比較好的JWT續期方案以及落地和大家分享一下,算是拋轉引玉,大家有好的方案歡迎留言哈。

後端

  1. Spring Cloud實戰 | 第一篇:Windows搭建Nacos服務
  2. Spring Cloud實戰 | 第二篇:Spring Cloud整合Nacos實現註冊中心
  3. Spring Cloud實戰 | 第三篇:Spring Cloud整合Nacos實現配置中心
  4. Spring Cloud實戰 | 第四篇:Spring Cloud整合Gateway實現API閘道器
  5. Spring Cloud實戰 | 第五篇:Spring Cloud整合OpenFeign實現微服務之間的呼叫
  6. Spring Cloud實戰 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT實現微服務統一認證授權
  7. Spring Cloud實戰 | 最七篇:Spring Cloud Gateway+Spring Security OAuth2整合統一認證授權平臺下實現登出使JWT失效方案
  8. Spring Cloud實戰 | 最八篇:Spring Cloud +Spring Security OAuth2+ Axios前後端分離模式下無感重新整理實現JWT續期

管理前端

  1. vue-element-admin實戰 | 第一篇: 移除mock接入後臺,搭建有來商城youlai-mall前後端分離管理平臺
  2. vue-element-admin實戰 | 第二篇: 最小改動接入後臺實現根據許可權動態載入選單

微信小程式

  1. vue+uniapp商城實戰 | 第一篇:【有來小店】微信小程式快速開發接入Spring Cloud OAuth2認證中心完成授權登入

二. 方案

理論背景:++有來商城++ 微服務專案 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個階段:

  1. 0~61s:雙token都沒過期,正常請求過程。

  1. 61s~120s:access_token過期,再次請求會執行一次重新整理請求。


  1. 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

相關文章