axios如何利用promise無(wú)痛刷新token(二)

16e6265fc4958f20.jpg

前言

前段時(shí)間寫了篇文章《axios如何利用promise無(wú)痛刷新token》,陸陸續(xù)續(xù)收到一些反饋。發(fā)現(xiàn)不少同學(xué)會(huì)想要從在請(qǐng)求前攔截的思路入手,甚至收到了幾個(gè)郵件來(lái)詢問(wèn)博主遇到的問(wèn)題,所以索性再寫一篇文章來(lái)說(shuō)說(shuō)另一個(gè)思路的實(shí)現(xiàn)和注意的地方。過(guò)程會(huì)稍微啰嗦,不想看實(shí)現(xiàn)過(guò)程的同學(xué)可以直接拉到最后面看最終代碼。

PS:在本文就略過(guò)一些前提條件了,請(qǐng)新同學(xué)閱讀本文前先看一下前一篇文章《axios如何利用promise無(wú)痛刷新token》。

前提條件

前端登錄后,后端返回token和token有效時(shí)間段tokenExprieIn,當(dāng)token過(guò)期時(shí)間到了,前端需要主動(dòng)用舊token去獲取一個(gè)新的token,做到用戶無(wú)感知地去刷新token。

PS: tokenExprieIn是一個(gè)單位為秒的時(shí)間段,不建議使用絕對(duì)時(shí)間,絕對(duì)時(shí)間可能會(huì)由于本地和服務(wù)器時(shí)區(qū)不一樣導(dǎo)致出現(xiàn)問(wèn)題。

實(shí)現(xiàn)思路

方法一

在請(qǐng)求發(fā)起前攔截每個(gè)請(qǐng)求,判斷token的有效時(shí)間是否已經(jīng)過(guò)期,若已過(guò)期,則將請(qǐng)求掛起,先刷新token后再繼續(xù)請(qǐng)求。

方法二

不在請(qǐng)求前攔截,而是攔截返回后的數(shù)據(jù)。先發(fā)起請(qǐng)求,接口返回過(guò)期后,先刷新token,再進(jìn)行一次重試。

前文已經(jīng)實(shí)現(xiàn)了方法二,本文會(huì)從頭實(shí)現(xiàn)一下方法一

實(shí)現(xiàn)

基本骨架

在請(qǐng)求前進(jìn)行攔截,我們主要會(huì)使用axios.interceptors.request.use()這個(gè)方法。照例先封裝個(gè)request.js的基本骨架:

import axios from 'axios'

// 從localStorage中獲取token,token存的是object信息,有tokenExpireTime和token兩個(gè)字段
function getToken () {
  let tokenObj = {}
  try {
    tokenObj = storage.get('token')
    tokenObj = tokenObj ? JSON.parse(tokenObj) : {}
  } catch {
    console.error('get token from localStorage error')
  }
  return tokenObj
}

// 給實(shí)例添加一個(gè)setToken方法,用于登錄后方便將最新token動(dòng)態(tài)添加到header,同時(shí)將token保存在localStorage中
instance.setToken = (obj) => {
  instance.defaults.headers['X-Token'] = obj.token
  window.localStorage.setItem('token', JSON.stringify(obj)) // 注意這里需要變成字符串后才能放到localStorage中
}

// 創(chuàng)建一個(gè)axios實(shí)例
const instance = axios.create({
  baseURL: '/api',
  timeout: 300000,
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  }
})

// 請(qǐng)求發(fā)起前攔截
instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 為每個(gè)請(qǐng)求添加token請(qǐng)求頭
  config.headers['X-Token'] = tokenObj.token
  
  // **接下來(lái)主要攔截的實(shí)現(xiàn)就在這里**
  
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

// 請(qǐng)求返回后攔截
instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    // token過(guò)期了,直接跳轉(zhuǎn)到登錄頁(yè) 
    window.location.href = '/'
  }
  return response
}, error => {
  console.log('catch', error)
  return Promise.reject(error)
})

export default instance

與前文略微不同的是,由于方法二不需要用到過(guò)期時(shí)間,所以前文localStorage中只存了token一個(gè)字符串,而方法一這里需要用到過(guò)期時(shí)間了,所以得存多一個(gè)數(shù)據(jù),因此localStorage中存的是Object類型的數(shù)據(jù),從localStorage中取值出來(lái)需要JSON.parse一下,為了防止發(fā)生錯(cuò)誤所以盡量使用try...catch。

axios.interceptors.request.use()實(shí)現(xiàn)

首先不需要想得太復(fù)雜,先不考慮多個(gè)請(qǐng)求同時(shí)進(jìn)來(lái)的情況,咱從最常見(jiàn)的場(chǎng)景入手:從localStorage拿到上一次存儲(chǔ)的過(guò)期時(shí)間,判斷是否已經(jīng)到了過(guò)期時(shí)間,是就立即刷新token然后再發(fā)起請(qǐng)求。

function refreshToken () {
    // instance是當(dāng)前request.js中已創(chuàng)建的axios實(shí)例
    return instance.post('/refreshtoken').then(res => res.data)
}

instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 為每個(gè)請(qǐng)求添加token請(qǐng)求頭
  config.headers['X-Token'] = tokenObj.token
  if (tokenObj.token && tokenObj.tokenExpireTime) {
      const now = Date.now()
      if (now >= tokenObj.tokenExpireTime) {
          // 當(dāng)前時(shí)間大于過(guò)期時(shí)間,說(shuō)明已經(jīng)過(guò)期了,返回一個(gè)Promise,執(zhí)行refreshToken后再return當(dāng)前的config
          return refreshToken().then(res => {
            const { token, tokenExprieIn } = res.data
            const tokenExpireTime = now + tokenExprieIn * 1000
            instance.setToken({ token, tokenExpireTime }) // 存token到localStorage
            console.log('刷新成功, return config即是恢復(fù)當(dāng)前請(qǐng)求')
            config.headers['X-Token'] = token // 將最新的token放到請(qǐng)求頭
            return config
          }).catch(res => {
            console.error('refresh token error: ', res)
          })
      }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

這里有兩個(gè)需要注意的地方:

  1. 之前說(shuō)到登錄或刷新token的接口返回的是一個(gè)單位為秒的時(shí)間段tokenExpireIn,而我們存到localStorage中的是已經(jīng)是一個(gè)基于當(dāng)前時(shí)間有效時(shí)間段算出的最終時(shí)間tokenExpireTime,是一個(gè)絕對(duì)時(shí)間,比如當(dāng)前時(shí)間是12點(diǎn),有效時(shí)間是3600秒(1個(gè)小時(shí)),則存到localStorage的過(guò)期時(shí)間是13點(diǎn)的時(shí)間戳,這樣可以少存一個(gè)當(dāng)前時(shí)間的字段到localStorage中,使用時(shí)只需要判斷該絕對(duì)時(shí)間即可。
  2. instance.interceptors.request.use中返回一個(gè)Promise,就可以使得該請(qǐng)求是先執(zhí)行refreshToken后再return config的,才能保證先刷新token后再真正發(fā)起請(qǐng)求。

其實(shí)博主直接運(yùn)行上面代碼后發(fā)現(xiàn)了一個(gè)嚴(yán)重錯(cuò)誤,進(jìn)入了一個(gè)死循環(huán)。這是因?yàn)椴┲鳑](méi)有注意到一個(gè)問(wèn)題:axios.interceptors.request.use()會(huì)攔截所有使用該實(shí)例發(fā)起的請(qǐng)求,即執(zhí)行refreshToken()時(shí)又一次進(jìn)入了axios.interceptors.request.use(),導(dǎo)致一直在return refreshToken()。

因此需要將刷新token和登錄這兩種情況排除出去,登錄和刷新token都不需要判斷是否過(guò)期的攔截,我們可以通過(guò)config.url來(lái)判斷是哪個(gè)接口:

instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 為每個(gè)請(qǐng)求添加token請(qǐng)求頭
  config.headers['X-Token'] = tokenObj.token
  // 登錄接口和刷新token接口繞過(guò)
  if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
    return config
  }
  if (tokenObj.token && tokenObj.tokenExpireTime) {
      const now = Date.now()
      if (now >= tokenObj.tokenExpireTime) {
          // 當(dāng)前時(shí)間大于過(guò)期時(shí)間,說(shuō)明已經(jīng)過(guò)期了,返回一個(gè)Promise,執(zhí)行refreshToken后再return當(dāng)前的config
          return refreshToken().then(res => {
            const { token, tokenExprieIn } = res.data
            const tokenExpireTime = now + tokenExprieIn * 1000
            instance.setToken({ token, tokenExpireTime }) // 存token到localStorage
            console.log('刷新成功, return config即是恢復(fù)當(dāng)前請(qǐng)求')
            config.headers['X-Token'] = token // 將最新的token放到請(qǐng)求頭
            return config
          }).catch(res => {
            console.error('refresh token error: ', res)
          })
      }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

問(wèn)題和優(yōu)化

接下來(lái)就是要考慮復(fù)雜一點(diǎn)的問(wèn)題了

防止多次刷新token

當(dāng)幾乎同時(shí)進(jìn)來(lái)兩個(gè)請(qǐng)求,為了避免多次執(zhí)行refreshToken,需要引入一個(gè)isRefreshing的進(jìn)行標(biāo)記:

let isRefreshing = false
instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 為每個(gè)請(qǐng)求添加token請(qǐng)求頭
  config.headers['X-Token'] = tokenObj.token
  // 登錄接口和刷新token接口繞過(guò)
  if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
    return config
  }
  if (tokenObj.token && tokenObj.tokenExpireTime) {
      const now = Date.now()
      if (now >= tokenObj.tokenExpireTime) {
          if (!isRefreshing) {
            isRefreshing = true
            return refreshToken().then(res => {
              const { token, tokenExprieIn } = res.data
              const tokenExpireTime = now + tokenExprieIn * 1000
              instance.setToken({ token, tokenExpireTime }) // 存token到localStorage
              isRefreshing = false //刷新成功,恢復(fù)標(biāo)志位
              config.headers['X-Token'] = token // 將最新的token放到請(qǐng)求頭
              return config
            }).catch(res => {
              console.error('refresh token error: ', res)
            })  
          }
      }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

多個(gè)請(qǐng)求時(shí)存到隊(duì)列中等刷新token后再發(fā)起

我們已經(jīng)知道了當(dāng)前已經(jīng)過(guò)期或者正在刷新token,此時(shí)再有請(qǐng)求發(fā)起,就應(yīng)該讓后面的這些請(qǐng)求等一等,等到refreshToken結(jié)束后再真正發(fā)起,所以需要用到一個(gè)Promise來(lái)讓它一直等。而后面的所有請(qǐng)求,我們將它們存放到一個(gè)requests的隊(duì)列中,等刷新token后再依次resolve。

instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 添加請(qǐng)求頭
  config.headers['X-Token'] = tokenObj.token
  // 登錄接口和刷新token接口繞過(guò)
  if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
    return config
  }
  if (tokenObj.token && tokenObj.tokenExpireTime) {
    const now = Date.now()
    if (now >= tokenObj.tokenExpireTime) {
      // 立即刷新token
      if (!isRefreshing) {
        console.log('刷新token ing')
        isRefreshing = true
        refreshToken().then(res => {
          const { token, tokenExprieIn } = res.data
          const tokenExpireTime = now + tokenExprieIn * 1000
          instance.setToken({ token, tokenExpireTime })
          isRefreshing = false
          return token
        }).then((token) => {
          console.log('刷新token成功,執(zhí)行隊(duì)列')
          requests.forEach(cb => cb(token))
          // 執(zhí)行完成后,清空隊(duì)列
          requests = []
        }).catch(res => {
          console.error('refresh token error: ', res)
        })
      }
      const retryOriginalRequest = new Promise((resolve) => {
        requests.push((token) => {
          // 因?yàn)閏onfig中的token是舊的,所以刷新token后要將新token傳進(jìn)來(lái)
          config.headers['X-Token'] = token
          resolve(config)
        })
      })
      return retryOriginalRequest
    }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

這里做了一點(diǎn)改動(dòng),注意到refreshToken()這一句前面去掉了return,而是改為了在后面return retryOriginalRequest,即當(dāng)發(fā)現(xiàn)有請(qǐng)求是過(guò)期的就存進(jìn)requests數(shù)組,等refreshToken結(jié)束后再執(zhí)行requests隊(duì)列,這是為了不影響原來(lái)的請(qǐng)求執(zhí)行次序。
我們假設(shè)同時(shí)有請(qǐng)求1,請(qǐng)求2請(qǐng)求3依次同時(shí)進(jìn)來(lái),我們希望是請(qǐng)求1發(fā)現(xiàn)過(guò)期,refreshToken后再依次執(zhí)行請(qǐng)求1,請(qǐng)求2請(qǐng)求3。
按之前return refreshToken()的寫法,會(huì)大概寫成這樣


  if (tokenObj.token && tokenObj.tokenExpireTime) {
    const now = Date.now()
    if (now >= tokenObj.tokenExpireTime) {
      // 立即刷新token
      if (!isRefreshing) {
        console.log('刷新token ing')
        isRefreshing = true
        return refreshToken().then(res => {
          const { token, tokenExprieIn } = res.data
          const tokenExpireTime = now + tokenExprieIn * 1000
          instance.setToken({ token, tokenExpireTime })
          isRefreshing = false
          config.headers['X-Token'] = token
          return config // 請(qǐng)求1
        }).catch(res => {
          console.error('refresh token error: ', res)
        }).finally(() => {
          console.log('執(zhí)行隊(duì)列')
          requests.forEach(cb => cb(token))
          // 執(zhí)行完成后,清空隊(duì)列
          requests = []
        })
      } else {
        // 只有請(qǐng)求2和請(qǐng)求3能進(jìn)入隊(duì)列
        const retryOriginalRequest = new Promise((resolve) => {
          requests.push((token) => {
            config.headers['X-Token'] = token
            resolve(config)
          })
        })
        return retryOriginalRequest
      }
    }
  }
  return config

隊(duì)列里面只有請(qǐng)求2請(qǐng)求3,代碼看起來(lái)應(yīng)該是return了請(qǐng)求1后,再在finally執(zhí)行隊(duì)列的,但實(shí)際的執(zhí)行順序會(huì)變成請(qǐng)求2,請(qǐng)求3,請(qǐng)求1,即請(qǐng)求1變成了最后一個(gè)執(zhí)行的,會(huì)改變執(zhí)行順序。

所以博主換了個(gè)思路,無(wú)論是哪個(gè)請(qǐng)求進(jìn)入了過(guò)期流程,我們都將請(qǐng)求放到隊(duì)列中,都return一個(gè)未resolve的Promise,等刷新token結(jié)束后再一一清算,這樣就可以保證請(qǐng)求1,請(qǐng)求2請(qǐng)求3這樣按原來(lái)順序執(zhí)行了。

這里多說(shuō)一句,可能很多剛接觸前端的同學(xué)無(wú)法理解requests.forEach(cb => cb(token))是如何執(zhí)行的。

// 我們先看一下,定義fn1
function fn1 () {
    console.log('執(zhí)行fn1')
}

// 執(zhí)行fn1,只需后面加個(gè)括號(hào)
fn1()

// 回歸到我們r(jià)equest數(shù)組中,每一項(xiàng)其實(shí)存的就是一個(gè)類似fn1的一個(gè)函數(shù)
const fn2 = (token) => {
    config.headers['X-Token'] = token
    resolve(config)
}

// 我們要執(zhí)行fn2,也只需在后面加個(gè)括號(hào)就可以了
fn2()

// 由于requests是一個(gè)數(shù)組,所以我們想遍歷執(zhí)行里面的所有的項(xiàng),所以用上了forEach
requests.forEach(fn => {
  // 執(zhí)行fn
  fn()
})

最后完整代碼

import axios from 'axios'

// 從localStorage中獲取token,token存的是object信息,有tokenExpireTime和token兩個(gè)字段
function getToken () {
  let tokenObj = {}
  try {
    tokenObj = storage.get('token')
    tokenObj = tokenObj ? JSON.parse(tokenObj) : {}
  } catch {
    console.error('get token from localStorage error')
  }
  return tokenObj
}

function refreshToken () {
    // instance是當(dāng)前request.js中已創(chuàng)建的axios實(shí)例
    return instance.post('/refreshtoken').then(res => res.data)
}

// 給實(shí)例添加一個(gè)setToken方法,用于登錄后方便將最新token動(dòng)態(tài)添加到header,同時(shí)將token保存在localStorage中
instance.setToken = (obj) => {
  instance.defaults.headers['X-Token'] = obj.token
  window.localStorage.setItem('token', JSON.stringify(obj)) // 注意這里需要變成字符串后才能放到localStorage中
}

instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 添加請(qǐng)求頭
  config.headers['X-Token'] = tokenObj.token
  // 登錄接口和刷新token接口繞過(guò)
  if (config.url.indexOf('/rereshToken') >= 0 || config.url.indexOf('/login') >= 0) {
    return config
  }
  if (tokenObj.token && tokenObj.tokenExpireTime) {
    const now = Date.now()
    if (now >= tokenObj.tokenExpireTime) {
      // 立即刷新token
      if (!isRefreshing) {
        console.log('刷新token ing')
        isRefreshing = true
        refreshToken().then(res => {
          const { token, tokenExprieIn } = res.data
          const tokenExpireTime = now + tokenExprieIn * 1000
          instance.setToken({ token, tokenExpireTime })
          isRefreshing = false
          return token
        }).then((token) => {
          console.log('刷新token成功,執(zhí)行隊(duì)列')
          requests.forEach(cb => cb(token))
          // 執(zhí)行完成后,清空隊(duì)列
          requests = []
        }).catch(res => {
          console.error('refresh token error: ', res)
        })
      }
      const retryOriginalRequest = new Promise((resolve) => {
        requests.push((token) => {
          // 因?yàn)閏onfig中的token是舊的,所以刷新token后要將新token傳進(jìn)來(lái)
          config.headers['X-Token'] = token
          resolve(config)
        })
      })
      return retryOriginalRequest
    }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

// 請(qǐng)求返回后攔截
instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    // token過(guò)期了,直接跳轉(zhuǎn)到登錄頁(yè) 
    window.location.href = '/'
  }
  return response
}, error => {
  console.log('catch', error)
  return Promise.reject(error)
})

export default instance

建議一步步調(diào)試的同學(xué),可以先去掉window.location.href = '/'這個(gè)跳轉(zhuǎn),保留log方便調(diào)試。

感謝看到最后,感謝點(diǎn)贊_

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容