axios如何利用promise無痛刷新token的實現(xiàn)方法
需求
最近遇到個需求:前端登錄后,后端返回token和token有效時間,當token過期時要求用舊token去獲取新的token,前端需要做到無痛刷新token,即請求刷新token時要做到用戶無感知。
需求解析
當用戶發(fā)起一個請求時,判斷token是否已過期,若已過期則先調(diào)refreshToken接口,拿到新的token后再繼續(xù)執(zhí)行之前的請求。
這個問題的難點在于:當同時發(fā)起多個請求,而刷新token的接口還沒返回,此時其他請求該如何處理?接下來會循序漸進地分享一下整個過程。
實現(xiàn)思路
由于后端返回了token的有效時間,可以有兩種方法:
方法一:
在請求發(fā)起前攔截每個請求,判斷token的有效時間是否已經(jīng)過期,若已過期,則將請求掛起,先刷新token后再繼續(xù)請求。
方法二:
不在請求前攔截,而是攔截返回后的數(shù)據(jù)。先發(fā)起請求,接口返回過期后,先刷新token,再進行一次重試。
兩種方法對比
方法一
- 優(yōu)點: 在請求前攔截,能節(jié)省請求,省流量。
- 缺點: 需要后端額外提供一個token過期時間的字段;使用了本地時間判斷,若本地時間被篡改,特別是本地時間比服務(wù)器時間慢時,攔截會失敗。
PS:token有效時間建議是時間段,類似緩存的MaxAge,而不要是絕對時間。當服務(wù)器和本地時間不一致時,絕對時間會有問題。
方法二
優(yōu)點:不需額外的token過期字段,不需判斷時間。
缺點: 會消耗多一次請求,耗流量。
綜上,方法一和二優(yōu)缺點是互補的,方法一有校驗失敗的風險(本地時間被篡改時,當然一般沒有用戶閑的蛋疼去改本地時間的啦),方法二更簡單粗暴,等知道服務(wù)器已經(jīng)過期了再重試一次,只是會耗多一個請求。
在這里博主選擇了 方法二。
實現(xiàn)
這里會使用axios來實現(xiàn),方法一是請求前攔截,所以會使用axios.interceptors.request.use()這個方法;
而方法二是請求后攔截,所以會使用axios.interceptors.response.use()方法。
封裝axios基本骨架
首先說明一下,項目中的token是存在localStorage中的。request.js基本骨架:
import axios from 'axios' // 從localStorage中獲取token function getLocalToken () { const token = window.localStorage.getItem('token') return token } // 給實例添加一個setToken方法,用于登錄后將最新token動態(tài)添加到header,同時將token保存在localStorage中 instance.setToken = (token) => { instance.defaults.headers['X-Token'] = token window.localStorage.setItem('token', token) } // 創(chuàng)建一個axios實例 const instance = axios.create({ baseURL: '/api', timeout: 300000, headers: { 'Content-Type': 'application/json', 'X-Token': getLocalToken() // headers塞token } }) // 攔截返回的數(shù)據(jù) instance.interceptors.response.use(response => { // 接下來會在這里進行token過期的邏輯處理 return response }, error => { return Promise.reject(error) }) export default instance
這個是項目中一般的axios實例的封裝,創(chuàng)建實例時,將本地已有的token放進header,然后export出去供調(diào)用。接下來就是如何攔截返回的數(shù)據(jù)啦。
instance.interceptors.response.use攔截實現(xiàn)
后端接口一般會有一個約定好的數(shù)據(jù)結(jié)構(gòu),如:
{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已經(jīng)帶上了/api,避免出現(xiàn)/api/api的情況 // 重試當前請求并返回promise return instance(config) }).catch(res => { console.error('refreshtoken error =>', res) //刷新token失敗,神仙也救不了了,跳轉(zhuǎn)到首頁重新登錄吧 window.location.href = '/' }) } return response }, error => { return Promise.reject(error) }) function refreshToken () { // instance是當前request.js中已創(chuàng)建的axios實例 return instance.post('/refreshtoken').then(res => res.data) }
這里需要額外注意的是,response.config就是原請求的配置,但這個是已經(jīng)處理過了的,config.url已經(jīng)帶上了baseUrl,因此重試時需要去掉,同時token也是舊的,需要刷新下。
以上就基本做到了無痛刷新token,當token正常時,正常返回,當token已過期,則axios內(nèi)部進行一次刷新token和重試。對調(diào)用者來說,axios內(nèi)部的刷新token是一個黑盒,是無感知的,因此需求已經(jīng)做到了。
問題和優(yōu)化
上面的代碼還是存在一些問題的,沒有考慮到多次請求的問題,因此需要進一步優(yōu)化。
如何防止多次刷新token
如果refreshToken接口還沒返回,此時再有一個過期的請求進來,上面的代碼就會再一次執(zhí)行refreshToken,這就會導(dǎo)致多次執(zhí)行刷新token的接口,因此需要防止這個問題。我們可以在request.js中用一個flag來標記當前是否正在刷新token的狀態(tài),如果正在刷新則不再調(diào)用刷新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時再進入方法了。但是這種做法是相當于把其他失敗的接口給舍棄了,假如同時發(fā)起兩個請求,且?guī)缀跬瑫r返回,第一個請求肯定是進入了refreshToken后再重試,而第二個請求則被丟棄了,仍是返回失敗,所以接下來還得解決其他接口的重試問題。
同時發(fā)起兩個或以上的請求時,其他接口如何重試
兩個接口幾乎同時發(fā)起和返回,第一個接口會進入刷新token后重試的流程,而第二個接口需要先存起來,然后等刷新token后再重試。同樣,如果同時發(fā)起三個請求,此時需要緩存后兩個接口,等刷新token后再重試。由于接口都是異步的,處理起來會有點麻煩。
當?shù)诙€過期的請求進來,token正在刷新,我們先將這個請求存到一個數(shù)組隊列中,想辦法讓這個請求處于等待中,一直等到刷新token后再逐個重試清空請求隊列。
那么如何做到讓這個請求處于等待中呢?為了解決這個問題,我們得借助Promise。將請求存進隊列中后,同時返回一個Promise,讓這個Promise一直處于Pending狀態(tài)(即不調(diào)用resolve),此時這個請求就會一直等啊等,只要我們不執(zhí)行resolve,這個請求就會一直在等待。當刷新請求的接口返回來后,我們再調(diào)用resolve,逐個重試。最終代碼:
// 是否正在刷新的標記 let isRefreshing = false // 重試隊列,每一項將是一個待執(zhí)行的函數(shù)形式 const 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 = '' // 已經(jīng)刷新了token,將所有隊列中的請求進行重試 requests.forEach(cb => cb(token)) return instance(config) }).catch(res => { console.error('refreshtoken error =>', res) window.location.href = '/' }).finally(() => { isRefreshing = false }) } else { // 正在刷新token,返回一個未執(zhí)行resolve的promise return new Promise((resolve) => { // 將resolve放進隊列,用一個函數(shù)形式來保存,等token刷新后直接執(zhí)行 requests.push((token) => { config.baseURL = '' config.headers['X-Token'] = token resolve(instance(config)) }) }) } } return response }, error => { return Promise.reject(error) })
這里可能比較難理解的是requests這個隊列中保存的是一個函數(shù),這是為了讓resolve不執(zhí)行,先存起來,等刷新token后更方便調(diào)用這個函數(shù)使得resolve執(zhí)行。至此,問題應(yīng)該都解決了。
最后完整代碼
import axios from 'axios' // 從localStorage中獲取token function getLocalToken () { const token = window.localStorage.getItem('token') return token } // 給實例添加一個setToken方法,用于登錄后將最新token動態(tài)添加到header,同時將token保存在localStorage中 instance.setToken = (token) => { instance.defaults.headers['X-Token'] = token window.localStorage.setItem('token', token) } function refreshToken () { // instance是當前request.js中已創(chuàng)建的axios實例 return instance.post('/refreshtoken').then(res => res.data) } // 創(chuàng)建一個axios實例 const instance = axios.create({ baseURL: '/api', timeout: 300000, headers: { 'Content-Type': 'application/json', 'X-Token': getLocalToken() // headers塞token } }) // 是否正在刷新的標記 let isRefreshing = false // 重試隊列,每一項將是一個待執(zhí)行的函數(shù)形式 const 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 = '' // 已經(jīng)刷新了token,將所有隊列中的請求進行重試 requests.forEach(cb => cb(token)) return instance(config) }).catch(res => { console.error('refreshtoken error =>', res) window.location.href = '/' }).finally(() => { isRefreshing = false }) } else { // 正在刷新token,將返回一個未執(zhí)行resolve的promise return new Promise((resolve) => { // 將resolve放進隊列,用一個函數(shù)形式來保存,等token刷新后直接執(zhí)行 requests.push((token) => { config.baseURL = '' config.headers['X-Token'] = token resolve(instance(config)) }) }) } } return response }, error => { return Promise.reject(error) }) export default instance
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Bootstrap項目實戰(zhàn)之首頁內(nèi)容介紹(全)
本文分為兩部分介紹Bootstrap首頁內(nèi)容介紹的實現(xiàn)代碼,感興趣的小伙伴們可以參考一下2016-04-04layui+ssm實現(xiàn)數(shù)據(jù)批量刪除功能
本篇文章給大家介紹layui+ssm實現(xiàn)數(shù)據(jù)批量刪除功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2023-12-12千分位數(shù)字格式化(用逗號隔開 代碼已做了修改 支持0-9位逗號隔開)的JS代碼
這篇文章主要介紹了千分位數(shù)字格式化的JS代碼,有需要的朋友可以參考一下2013-12-12javascript實現(xiàn)數(shù)組中的內(nèi)容隨機輸出
本文實例講述了javaScript數(shù)組隨機排列實現(xiàn)隨機洗牌功能的方法。分享給大家供大家參考。2015-08-08