最近遇到個需求:前端登陸後,後端返回token
和token有效時間
,當token過時時要求用舊token去獲取新的token,前端須要作到無痛刷新token
,即請求刷新token時要作到用戶無感知。前端
當用戶發起一個請求時,判斷token是否已過時,若已過時則先調refreshToken
接口,拿到新的token後再繼續執行以前的請求。ios
這個問題的難點在於:當同時發起多個請求,而刷新token的接口還沒返回,此時其餘請求該如何處理?接下來會按部就班地分享一下整個過程。git
因爲後端返回了token的有效時間,能夠有兩種方法:github
在請求發起前攔截每一個請求,判斷token的有效時間是否已通過期,若已過時,則將請求掛起,先刷新token後再繼續請求。json
不在請求前攔截,而是攔截返回後的數據。先發起請求,接口返回過時後,先刷新token,再進行一次重試。axios
方法一後端
PS:token有效時間建議是時間段,相似緩存的MaxAge,而不要是絕對時間。當服務器和本地時間不一致時,絕對時間會有問題。
方法二api
綜上,方法一和二優缺點是互補的,方法一有校驗失敗的風險(本地時間被篡改時,固然通常沒有用戶閒的蛋疼去改本地時間的啦),方法二更簡單粗暴,等知道服務器已通過期了再重試一次,只是會耗多一個請求。數組
在這裏博主選擇了 方法二。promise
這裏會使用axios來實現,方法一是請求前攔截,因此會使用axios.interceptors.request.use()
這個方法;
而方法二是請求後攔截,因此會使用axios.interceptors.response.use()
方法。
首先說明一下,項目中的token是存在localStorage
中的。request.js
基本骨架:
import axios from 'axios' // 從localStorage中獲取token function getLocalToken () { const token = window.localStorage.getItem('token') return token } // 給實例添加一個setToken方法,用於登陸後將最新token動態添加到header,同時將token保存在localStorage中 instance.setToken = (token) => { instance.defaults.headers['X-Token'] = token window.localStorage.setItem('token', token) } // 建立一個axios實例 const instance = axios.create({ baseURL: '/api', timeout: 300000, headers: { 'Content-Type': 'application/json', 'X-Token': getLocalToken() // headers塞token } }) // 攔截返回的數據 instance.interceptors.response.use(response => { // 接下來會在這裏進行token過時的邏輯處理 return response }, error => { return Promise.reject(error) }) export default instance
這個是項目中通常的axios實例的封裝,建立實例時,將本地已有的token放進header,而後export出去供調用。接下來就是如何攔截返回的數據啦。
後端接口通常會有一個約定好的數據結構,如:
{code: 1234, message: 'token過時', data: {}}
如我這裏,後端約定當code === 1234
時表示token過時了,此時就要求刷新token。
instance.interceptors.response.use(response => { const { code } = response.data if (code === 1234) { // 說明token過時了,刷新token return refreshToken().then(res => { // 刷新token成功,將最新的token更新到header中,同時保存在localStorage中 const { token } = res.data instance.setToken(token) // 獲取當前失敗的請求 const config = response.config // 重置一下配置 config.headers['X-Token'] = token config.baseURL = '' // url已經帶上了/api,避免出現/api/api的狀況 // 重試當前請求並返回promise return instance(config) }).catch(res => { console.error('refreshtoken error =>', res) //刷新token失敗,神仙也救不了了,跳轉到首頁從新登陸吧 window.location.href = '/' }) } return response }, error => { return Promise.reject(error) }) function refreshToken () { // instance是當前request.js中已建立的axios實例 return instance.post('/refreshtoken').then(res => res.data) }
這裏須要額外注意的是,response.config
就是原請求的配置,但這個是已經處理過了的,config.url
已經帶上了baseUrl
,所以重試時須要去掉,同時token也是舊的,須要刷新下。
以上就基本作到了無痛刷新token,當token正常時,正常返回,當token已過時,則axios內部進行一次刷新token和重試。對調用者來講,axios內部的刷新token是一個黑盒,是無感知的,所以需求已經作到了。
上面的代碼仍是存在一些問題的,沒有考慮到屢次請求的問題,所以須要進一步優化。
若是refreshToken接口還沒返回,此時再有一個過時的請求進來,上面的代碼就會再一次執行refreshToken,這就會致使屢次執行刷新token的接口,所以須要防止這個問題。咱們能夠在request.js
中用一個flag
來標記當前是否正在刷新token的狀態,若是正在刷新則再也不調用刷新token的接口。
// 是否正在刷新的標記 let isRefreshing = false instance.interceptors.response.use(response => { const { code } = response.data if (code === 1234) { if (!isRefreshing) { isRefreshing = true return refreshToken().then(res => { const { token } = res.data instance.setToken(token) const config = response.config config.headers['X-Token'] = token config.baseURL = '' return instance(config) }).catch(res => { console.error('refreshtoken error =>', res) window.location.href = '/' }).finally(() => { isRefreshing = false }) } } return response }, error => { return Promise.reject(error) })
這樣子就能夠避免在刷新token時再進入方法了。可是這種作法是至關於把其餘失敗的接口給捨棄了,假如同時發起兩個請求,且幾乎同時返回,第一個請求確定是進入了refreshToken後再重試,而第二個請求則被丟棄了,還是返回失敗,因此接下來還得解決其餘接口的重試問題。
兩個接口幾乎同時發起和返回,第一個接口會進入刷新token後重試的流程,而第二個接口須要先存起來,而後等刷新token後再重試。一樣,若是同時發起三個請求,此時須要緩存後兩個接口,等刷新token後再重試。因爲接口都是異步的,處理起來會有點麻煩。
當第二個過時的請求進來,token正在刷新,咱們先將這個請求存到一個數組隊列中,想辦法讓這個請求處於等待中,一直等到刷新token後再逐個重試清空請求隊列。
那麼如何作到讓這個請求處於等待中呢?爲了解決這個問題,咱們得藉助Promise
。將請求存進隊列中後,同時返回一個Promise
,讓這個Promise
一直處於Pending
狀態(即不調用resolve),此時這個請求就會一直等啊等,只要咱們不執行resolve,這個請求就會一直在等待。當刷新請求的接口返回來後,咱們再調用resolve,逐個重試。最終代碼:
// 是否正在刷新的標記 let isRefreshing = false // 重試隊列,每一項將是一個待執行的函數形式 let requests = [] instance.interceptors.response.use(response => { const { code } = response.data if (code === 1234) { const config = response.config if (!isRefreshing) { isRefreshing = true return refreshToken().then(res => { const { token } = res.data instance.setToken(token) config.headers['X-Token'] = token config.baseURL = '' // 已經刷新了token,將全部隊列中的請求進行重試 requests.forEach(cb => cb(token)) // 重試完了別忘了清空這個隊列(掘金評論區同窗指點) requests = [] return instance(config) }).catch(res => { console.error('refreshtoken error =>', res) window.location.href = '/' }).finally(() => { isRefreshing = false }) } else { // 正在刷新token,返回一個未執行resolve的promise return new Promise((resolve) => { // 將resolve放進隊列,用一個函數形式來保存,等token刷新後直接執行 requests.push((token) => { config.baseURL = '' config.headers['X-Token'] = token resolve(instance(config)) }) }) } } return response }, error => { return Promise.reject(error) })
這裏可能比較難理解的是requests
這個隊列中保存的是一個函數,這是爲了讓resolve不執行,先存起來,等刷新token後更方便調用這個函數使得resolve執行。至此,問題應該都解決了。
import axios from 'axios' // 從localStorage中獲取token function getLocalToken () { const token = window.localStorage.getItem('token') return token } // 給實例添加一個setToken方法,用於登陸後將最新token動態添加到header,同時將token保存在localStorage中 instance.setToken = (token) => { instance.defaults.headers['X-Token'] = token window.localStorage.setItem('token', token) } function refreshToken () { // instance是當前request.js中已建立的axios實例 return instance.post('/refreshtoken').then(res => res.data) } // 建立一個axios實例 const instance = axios.create({ baseURL: '/api', timeout: 300000, headers: { 'Content-Type': 'application/json', 'X-Token': getLocalToken() // headers塞token } }) // 是否正在刷新的標記 let isRefreshing = false // 重試隊列,每一項將是一個待執行的函數形式 let requests = [] instance.interceptors.response.use(response => { const { code } = response.data if (code === 1234) { const config = response.config if (!isRefreshing) { isRefreshing = true return refreshToken().then(res => { const { token } = res.data instance.setToken(token) config.headers['X-Token'] = token config.baseURL = '' // 已經刷新了token,將全部隊列中的請求進行重試 requests.forEach(cb => cb(token)) requests = [] return instance(config) }).catch(res => { console.error('refreshtoken error =>', res) window.location.href = '/' }).finally(() => { isRefreshing = false }) } else { // 正在刷新token,將返回一個未執行resolve的promise return new Promise((resolve) => { // 將resolve放進隊列,用一個函數形式來保存,等token刷新後直接執行 requests.push((token) => { config.baseURL = '' config.headers['X-Token'] = token resolve(instance(config)) }) }) } } return response }, error => { return Promise.reject(error) }) export default instance
但願對你們有幫助。感謝看到最後,感謝點贊^_^。