Spring Cloud實戰 | 最八篇:Spring Cloud +Spring Security OAuth2+ Vue先後端分離模式下無感知刷新實現JWT續期

一. 前言

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

本篇就我的以爲比較好的JWT續期方案以及落地和你們分享一下,算是拋轉引玉,你們有好的方案歡迎留言哈。前端

後端vue

  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+ Vue先後端分離模式下無感知刷新實現JWT續期

管理前端ios

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

微信小程序git

  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返回給客戶端,可是爲了客戶良好的體驗須要作到無感知刷新。github

方案一:spring

瀏覽器起一個定時輪詢任務,每次在access_token過時以前刷新。axios

方案二:小程序

請求時返回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
相關文章
相關標籤/搜索