天天看點

Springboot+Axios雙token解決token過期續簽問題

作者:德才兼備清風Q

前後端分離使用token進行登入驗證時,由于token存在過期時間,每次token過期都需要使用者重新登入的話,使用者體驗很不友好。假如token能跟session一樣,如果使用者持續在進行操作,就自動延長有效時間,就可以解決問題。但是,token一旦簽發,伺服器就沒法再延長token的有效期,目前用的比較多的應該是使用雙token實作token續簽,當token過期時,簽發新的token給前端,前端攜帶新的token請求後端接口。

具體思路:在簽發token時生成兩個token,accessToken和refreshToken,前端每次請求時攜帶accessToken,後端發現accessToken過期時,傳回token已過期的結果。前端根據後端狀态碼判斷token是否已經過期,如果過期則攜帶refreshToken請求重新整理token的接口,如果refreshToken沒有過期,則後端重新生成accessToken和refreshToken傳回給前端;如果refreshToken也過期了,則傳回結果要求前端重新登入。

後端實作

accessToken設定過期時間為30分鐘,refreshToken設定過期時間為60分鐘,這樣的話accessToken過期後的30分鐘内使用者有操作,仍可以使用refreshToken請求重新整理token。

Springboot+Axios雙token解決token過期續簽問題

建立accessToken

Springboot+Axios雙token解決token過期續簽問題

建立refreshToken

JWT解析token,token過期則傳回-1,其他解析錯誤則傳回-2,解析成功傳回1。

Springboot+Axios雙token解決token過期續簽問題

LoginController驗證賬号密碼成功後,建立accessToken和refreshToken傳回前端,将accessToken和refreshToken儲存在redis中。為避免使用者登出或更換裝置登入後,舊的accessToken和refreshToken還沒有過期,仍然能生效,在redis中使用user的id為鍵儲存的accessToken和refreshToken,每次登入後都會将原來的進行覆寫,這樣隻需要在攔截器中将token與reids中進行比對,如果比對不一緻,則不放行。

Springboot+Axios雙token解決token過期續簽問題

登入生成accessToken和refreshToken

LoginInterceptor驗證accessToken,如果傳回-1,則表示accessToken過期,提示使用者需要重新整理token。為了避免使用者在新裝置登入,舊裝置的accessToken仍然有效,每次校驗完accessToken成功,都還要在redis中查找是否存在以id為key的記錄,并且将redis中取出的redisToken和accessToken對比是否一緻,如果沒有或不一緻,則表示accessToken已經被redis廢棄,仍不能放行,傳回用戶端資訊為該賬号已在其他裝置登入,請重新登入。

Springboot+Axios雙token解決token過期續簽問題

代碼截不全,主要邏輯都在

refreshToken接口。重新整理token的接口/api/refresh用于前端調用。首先重新整理token的接口要在Interceptor中放行,避免refreshToken過期後,傳回結果仍然是需要重新整理token。隻有refreshToken解析成功并且與redis中的refreshToken一緻時,才會重新簽發accessToken和refreshToken。

Springboot+Axios雙token解決token過期續簽問題

重新整理token的接口

前端實作

前端實作靠axios的請求攔截器和響應攔截器,請求攔截器配置每次請求攜帶token,主要的難題在于多請求下響應攔截器的處理。

具體思路:設定一個重新整理token的狀态isRefreshAvailable并設定為true,同時發出多個請求時,token過期都會由後端傳回需要重新整理token的資訊,那麼,當第一個響應回來進入重新整理token程式後,将isRefreshAvailable設定為false,其他請求都不能再發起重新整理token請求,使用promise将剩下的請求放入一個緩存數組,當重新整理token結束後再周遊數組将緩存的請求逐個發出。

由于前端知識不足,網上查了不少辦法,主要出現兩個問題,問題的分析不知道是否正确,最後用了setTimeout延遲3秒再将isRefreshAvailable設定為true并且重新發送緩存的請求,沒發現再出現以下兩個問題。如果有好的解決辦法,請不吝賜教,萬分感激:

問題1、重新整理token接口多次被調用。調用了7個請求系統時間接口的請求,按照網上的辦法,調用重新整理token接口得到新的accessToken和refreshToken後就将isRefreshAvailable設定為true,但有的原始請求響應晚于重新整理token的請求響應,造成多次調用重新整理token接口,而後端即便token解析成功也會從redis中進行比對,造成重發的請求攜帶的accessToken與redis中不一緻,比對失敗傳回重新登入頁面。解決問題的關鍵在于何時改變isRefreshAvailable的狀态。

問題2、請求丢失的問題。原始請求因為傳回結果較晚,當重新整理完token開始周遊緩存數組的時候,有的原始請求結果才傳回,這樣即便進了數組,也沒有能夠重新發送。

Springboot+Axios雙token解決token過期續簽問題

發送了7個系統時間請求,重新整理token後隻重發了2個

前端代碼:

accessToken和refreshToken存放在sessionStorage中,擷取accessToken和refreshToken的以及清空sessionStorage到登入頁面的函數:

Springboot+Axios雙token解決token過期續簽問題
function getAccessToken () {
  return window.sessionStorage.getItem('token')
}

function getRefreshToken () {
  return window.sessionStorage.getItem('refreshToken')
}

function toLogin () {
  setTimeout(() => {
    window.sessionStorage.clear()
    isRefreshAvailable = true
    requestAttr = []
    window.location.href = '/login'
  }, 3000)
}           

重新整理token的函數:獲得重新整理後的accessToken和refreshToken後,儲存到sessionStorage中,得到新的token後這裡先不設定isRefreshAvailable為ture。

Springboot+Axios雙token解決token過期續簽問題

重新整理token函數

async function refreshToken () {
  try {
    var result = await http({
      url: '/test/refresh',
      method: 'post',
      headers: {
        Refresh: getRefreshToken()
      }
    })
  } catch (e) {
    messageOnce.error({ message: '自動擷取授權失敗! 3秒後自動跳轉至登入界面' })
    toLogin()
  }
  if (result.status === 200) {
    window.sessionStorage.setItem('token', result.accessToken)
    window.sessionStorage.setItem('refreshToken', result.refreshToken)
  }
}           

請求攔截器:每次請求都在請求頭中設定Authorization字段攜帶token,這裡使用了element ui的Loading加載元件,為了確定所有的ajax請求響應後再關閉Loading,使用了loadCount進行計數,每發起一個請求,loadCount加1。

Springboot+Axios雙token解決token過期續簽問題

請求攔截器

http.interceptors.request.use(
  config => {
    var token = getAccessToken()
    token && (config.headers.Authorization = token)
    loadCount++
    loadingInstance = Loading.service({
      text: '正在加載...'
    })
    return config
  },
  error => {
    loadingInstance.close()
    messageOnce.warning({ message: '請求逾時' })
    return Promise.reject(error)
  }
)
// 是否可以重新整理辨別
let isRefreshAvailable = true
// 緩存請求的數組
let requestAttr = []           

響應攔截器:當後端響應token相關錯誤的狀态碼時,10001代表沒有token,10002代表token解析失敗,10003代表refreshToken過期,清空sessionStorage并自動跳轉至登入界面。這裡每有一個請求得到響應,就将loadCount減1,當loadCount為0,且isRefreshAvailable為true時,關閉Loading元件。當後端響應accessToken過期的10000時,根據isRefreshAvailable判斷是否正在重新整理token,isRefreshAvailable為true,表示可以重新整理token,調用重新整理token的refreshToken函數,并将isRefreshAvailable設定為false,其他響應不能再調用refreshToken函數。為了避免前述的token多次重新整理和請求丢失的兩個問題,重新整理完token,3秒後再将緩存數組中的請求進行重發,并且将isRefreshAvailable設定為true。

Springboot+Axios雙token解決token過期續簽問題

響應攔截器

如果其他token過期的響應回來時正在重新整理token,則使用promise将請求存入緩存數組requestAttr,如果不是token相關的錯誤狀态碼,則列印錯誤結果,如果狀态碼為成功200,則将響應資料傳回。

Springboot+Axios雙token解決token過期續簽問題

響應攔截器

http.interceptors.response.use(
  response => {
    loadCount--
    if (loadCount === 0 && isRefreshAvailable === true) {
      loadingInstance.close()
    }
    if (response.data.status === 10001 || response.data.status === 10002 || response.data.status === 10003) {
      messageOnce.error({ message: response.data.message + '! 3秒後自動跳轉至登入界面' })
      toLogin()
    } else if (response.data.status === 10000) {
      if (isRefreshAvailable) {
        isRefreshAvailable = false
        refreshToken()
        // 拿到新accessToken後,等待2-3秒,確定其他請求響應都回來後再重新發送請求
        // 防止重發數組請求後才有請求傳回,丢失該部分請求
        setTimeout(() => {
          console.log('開始重新發起請求')
          requestAttr.forEach((cb) => cb(getAccessToken()))
          requestAttr = []
          isRefreshAvailable = true
          response.config.headers.Authorization = 'Bearer' + getAccessToken()
          return http(response.config)
        }, 3000)
      } else {
        return new Promise(resolve => {
          requestAttr.push((token) => {
            console.log('緩存數組的數量:', requestAttr.length)
            response.config.headers.Authorization = 'Bearer' + token
            resolve(http(response.config))
          })
        })
      }
    } else if (response.data.status !== 200) {
      messageOnce.warning({ message: response.data.message })
    } else {
      return response.data
    }
  },
  error => {
    // 對響應錯誤做點什麼
    loadCount = 0
    loadingInstance.close()
    messageOnce.error({ message: '與伺服器連接配接發生錯誤' })
    return Promise.resolve(error)
  }
)           

最終效果,發出7個請求系統時間的請求,得到7個需要重新整理token的響應,隻調用了一次refresh接口,又重新發送了7個請求系統時間的請求。

Springboot+Axios雙token解決token過期續簽問題

正确的結果

PS:這個代碼塊對手機支援不太友好啊,預覽了下,過寬的代碼塊沒有出現滾動條啊。