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