利用Axios實(shí)現(xiàn)無感知雙Token刷新的詳細(xì)教程
一、引言
在現(xiàn)代系統(tǒng)中,Token認(rèn)證已成為保障用戶安全的標(biāo)準(zhǔn)做法。然而,盡管許多系統(tǒng)采用了這種認(rèn)證方式,卻在處理Token刷新方面存在不足,導(dǎo)致用戶體驗(yàn)不佳。隨著Token有效期的縮短,頻繁的重新登錄成為常見現(xiàn)象,許多系統(tǒng)未能提供一種無縫的、用戶無感知的Token刷新機(jī)制。通過結(jié)合Vue3和Axios這兩大前端技術(shù)棧,我們可以借助Promise機(jī)制,開發(fā)出一種更加完善的自動(dòng)化Token刷新方案,顯著提升系統(tǒng)的穩(wěn)定性和用戶體驗(yàn)。本文將深入探討這一實(shí)現(xiàn)過程,幫助你解決Token刷新難題。
二、示意圖
三、具體實(shí)現(xiàn)
了解了基本步驟后,實(shí)際的實(shí)現(xiàn)過程其實(shí)相當(dāng)簡潔。然而,在具體操作中,仍有許多關(guān)鍵細(xì)節(jié)需要我們仔細(xì)考量,以確保Token刷新機(jī)制的穩(wěn)定性和可靠性。
- Token 存儲(chǔ)與管理:首先,明確如何安全地存儲(chǔ)和管理Access Token與Refresh Token。這涉及到瀏覽器的存儲(chǔ)策略,比如使用
localStorage
、sessionStorage
,存儲(chǔ)策略不在本文中提及,本文采用localStorage 進(jìn)行存儲(chǔ)。 - 請(qǐng)求攔截器的設(shè)置:在Axios中設(shè)置請(qǐng)求攔截器,用于在每次發(fā)送請(qǐng)求前檢查Token的有效性。如果發(fā)現(xiàn)Token過期,則觸發(fā)刷新流程。這一步驟需注意避免并發(fā)請(qǐng)求引發(fā)的重復(fù)刷新。
- 處理Token刷新的響應(yīng)邏輯:當(dāng)Token過期時(shí),通過發(fā)送Refresh Token請(qǐng)求獲取新的Access Token。在這里,需要處理刷新失敗的情況,如Refresh Token也失效時(shí),如何引導(dǎo)用戶重新登錄。
- 隊(duì)列機(jī)制的引入:在Token刷新過程中,可能會(huì)有多個(gè)請(qǐng)求被同時(shí)發(fā)出。為了避免重復(fù)刷新Token,可以引入隊(duì)列機(jī)制,確保在刷新Token期間,其他請(qǐng)求被掛起,直到新的Token可用。
- 錯(cuò)誤處理與用戶體驗(yàn):最后,要對(duì)整個(gè)流程中的錯(cuò)誤進(jìn)行處理,比如刷新失敗后的重試邏輯、錯(cuò)誤提示信息等,確保用戶體驗(yàn)不受影響。
通過以上步驟的實(shí)現(xiàn),你可以構(gòu)建一個(gè)用戶無感知、穩(wěn)定可靠的雙Token刷新機(jī)制,提升應(yīng)用的安全性與用戶體驗(yàn)。接下來,我們將逐一解析這些關(guān)鍵步驟的具體實(shí)現(xiàn)。
1. 編寫請(qǐng)求攔截器
實(shí)現(xiàn)請(qǐng)求攔截器的基本邏輯比較簡單,即在每次請(qǐng)求時(shí)自動(dòng)附帶上Token以進(jìn)行認(rèn)證。
service.interceptors.request.use((config: InternalAxiosRequestConfig) => { const userStore = useUserStore() if (userStore.authInfo.accessToken && userStore.authInfo.accessToken !== "") { // 設(shè)置頭部 token config.headers.Authorization = RequestConstant.Header.AuthorizationPrefix + userStore.authInfo.accessToken; } return config; }, (error: any) => { return Promise.reject(error); } );
目前的實(shí)現(xiàn)方案是,在請(qǐng)求存在有效Token時(shí),將其附帶到請(qǐng)求頭中發(fā)送給服務(wù)器。但在一些特殊情況下,某些請(qǐng)求可能不需要攜帶Token。為此,我們可以在請(qǐng)求配置中通過config
對(duì)象來判斷是否需要攜帶Token。例如:
request: (deptId: number, deptForm: DeptForm): AxiosPromise<void> => { return request<void>({ url: DeptAPI.UPDATE.endpoint(deptId), method: "put", data: deptForm, headers: { // 根據(jù)需要添加Token,或者通過自定義邏輯決定是否包含Authorization字段 token: false } }); }
那么在請(qǐng)求攔截器中,您需要多加一個(gè)判斷,就是判斷請(qǐng)求頭中token是否需要
// 代碼省略
2. 深究響應(yīng)攔截器
對(duì)于雙token刷新的難點(diǎn)就在于響應(yīng)攔截器中,因?yàn)樵谶@里后端會(huì)返回token過期的信息。我們需要先清楚后端接口響應(yīng)內(nèi)容
2.1 接口介紹
- 正常接口響應(yīng)內(nèi)容
// Status Code: 200 OK { "code":"0000", "msg":"操作成功", "data":{} }
- accessToken 過期響應(yīng)內(nèi)容
// Status Code: 401 Unauthorized { "code":"I009", "msg":"登錄令牌過期" }
- accessToken 刷新響應(yīng)內(nèi)容
// Status Code: 200 OK { "code": "0000", "msg": "操作成功", "data": { "accessToken": "", "refreshToken": "", "expires": "" } }
- refreshToken 過期響應(yīng)內(nèi)容
// Status Code: 200 OK { "code": "I009", "msg": "登錄令牌過期" }
注意 : 當(dāng)Status Code
不是200時(shí),Axios的響應(yīng)攔截器會(huì)自動(dòng)進(jìn)入error
方法。在這里,我們可以捕捉到HTTP狀態(tài)碼為401的請(qǐng)求,從而初步判斷請(qǐng)求是由于Unauthorized
(未授權(quán))引發(fā)的。然而,觸發(fā)401狀態(tài)碼的原因有很多,不一定都代表Token過期。因此,為了準(zhǔn)確判斷Token是否真的過期,我們需要進(jìn)一步檢查響應(yīng)體中的code
字段。
2.2 響應(yīng)攔截器編寫
有上面的接口介紹,我們編寫的就簡單,判斷error.response?.status === 401、code === I009 即可,如果出現(xiàn)這種情況就直接刷新token。
service.interceptors.response.use(async (response: AxiosResponse) => { // 正常請(qǐng)求代碼忽略 return Promise.reject(new Error(msg || "Error")); }, async (error: any) => { const userStore = useUserStore() if (error.response?.status === 401) { if (error.response?.data?.code === RequestConstant.Code.AUTH_TOKEN_EXPIRED) { // token 過期處理 // 1. 刷新 token const loginResult: LoginResult = await userStore.refreshToken() if (loginResult) { // refreshToken 未過期 // 2.1 重構(gòu)請(qǐng)求頭 error.config.headers.Authorization = RequestConstant.Header.AuthorizationPrefix + userStore.authInfo.accessToken; // 2.2 請(qǐng)求 return await service.request(error.config); } else { // refreshToken 過期 // 1. 重置登錄 token , 跳轉(zhuǎn)登錄頁 await userStore.resetToken() } } else { // 如果是系統(tǒng)發(fā)出的401 , 重置登錄 token , 跳轉(zhuǎn)登錄頁 await userStore.resetToken() } } else if (error.response?.status === 403) { // 403 結(jié)果處理 , 代碼省略 } else { // 其他錯(cuò)誤結(jié)果處理 , 代碼省略 } return Promise.reject(error.message); } );
2.3 解決重復(fù)刷新問題
編寫完成上面的內(nèi)容,考慮一下多個(gè)請(qǐng)求可能同時(shí)遇到 Token 過期,如果沒有適當(dāng)?shù)臋C(jī)制控制,這些請(qǐng)求可能會(huì)同時(shí)發(fā)起刷新 Token 的操作,導(dǎo)致重復(fù)請(qǐng)求,甚至可能觸發(fā)后端的安全機(jī)制將這些請(qǐng)求標(biāo)記為危險(xiǎn)操作。
為了解決這個(gè)問題,我們實(shí)現(xiàn)了一個(gè)單例 Promise
的刷新邏輯,通過 singletonRefreshToken
確保在同一時(shí)間只有一個(gè)請(qǐng)求會(huì)發(fā)起 Token 刷新操作。其核心思想是讓所有需要刷新的請(qǐng)求共享同一個(gè) Promise
,這樣即使有多個(gè)請(qǐng)求同時(shí)遇到 Token 過期,它們也只會(huì)等待同一個(gè)刷新操作的結(jié)果,而不會(huì)導(dǎo)致多次刷新。
/** * 刷新 token */ refreshToken(): Promise<LoginResult> { // 如果 singletonRefreshToken 不為 null 說明已經(jīng)在刷新中,直接返回 if (singletonRefreshToken !== null) { return singletonRefreshToken } // 設(shè)置 singletonRefreshToken 為一個(gè) Promise 對(duì)象 , 處理刷新 token 請(qǐng)求 singletonRefreshToken = new Promise<LoginResult>(async (resolve) => { await AuthAPI.REFRESH.request({ accessToken: this.authInfo.accessToken as string, refreshToken: this.authInfo.refreshToken as string }).then(({data}) => { // 設(shè)置刷新后的Token this.authInfo = data // 刷新路由 resolve(data) }).catch(() => { this.resetToken() }) }) // 最終將 singletonRefreshToken 設(shè)置為 null, 防止 singletonRefreshToken 一直占用 singletonRefreshToken.finally(() => { singletonRefreshToken = null; }) return singletonRefreshToken }
重要點(diǎn)解析:
singletonRefreshToken
的使用:singletonRefreshToken
是一個(gè)全局變量,用于保存當(dāng)前正在進(jìn)行的刷新操作。如果某個(gè)請(qǐng)求發(fā)現(xiàn)singletonRefreshToken
不為null
,就說明另一個(gè)請(qǐng)求已經(jīng)發(fā)起了刷新操作,它只需等待這個(gè)操作完成,而不需要自己再發(fā)起新的刷新請(qǐng)求。
共享同一個(gè)
Promise
:- 當(dāng)
singletonRefreshToken
被賦值為一個(gè)新的Promise
時(shí),所有遇到 Token 過期的請(qǐng)求都會(huì)返回這個(gè)Promise
,并等待它的結(jié)果。這樣就避免了同時(shí)發(fā)起多個(gè)刷新請(qǐng)求。
- 當(dāng)
刷新完成后的處理:
- 刷新操作完成后(無論成功與否),都會(huì)通過
finally
將singletonRefreshToken
置為null
,從而確保下一次 Token 過期時(shí)能夠重新發(fā)起刷新請(qǐng)求。
- 刷新操作完成后(無論成功與否),都會(huì)通過
通過這種機(jī)制,我們可以有效地避免重復(fù)刷新 Token 的問題,同時(shí)也防止了由于過多重復(fù)請(qǐng)求而引發(fā)的后端安全性問題。這種方法不僅提高了系統(tǒng)的穩(wěn)定性,還優(yōu)化了資源使用,確保了用戶的請(qǐng)求能夠正確地處理。
四、測試
- 當(dāng)我們攜帶過期token訪問接口,后端就會(huì)返回401狀態(tài)和I009。
這時(shí)候進(jìn)入
const loginResult: LoginResult = await userStore.refreshToken()
- 攜帶之前過期的accessToken和未過期的refreshToken進(jìn)行刷新
- 攜帶過期的accessToken的原因 :
- 防止未過期的 accessToken 進(jìn)行刷新
- 防止 accessToken 和 refreshToken 不是同一用戶發(fā)出的
- 其他安全性考慮
- 獲取到正常結(jié)果
五、源碼
前端源碼位置 : yf/ yf-vue-admin / src / utils / request.ts
后端源碼位置 : yf/ .. / impl / AuthServiceImpl.java
以上就是利用Axios實(shí)現(xiàn)無感知雙Token刷新的詳細(xì)教程的詳細(xì)內(nèi)容,更多關(guān)于Axios無感知Token刷新的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JS?getRandomValues和Math.random方法深入解析
這篇文章主要為大家介紹了JS?getRandomValues和Math.random方法深入詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04基于JavaScript實(shí)現(xiàn)貪吃蛇游戲
這篇文章主要為大家詳細(xì)介紹了基于JavaScript實(shí)現(xiàn)貪吃蛇游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03localStorage設(shè)置有效期和過期時(shí)間的簡單方法
眾所周知前端三大緩存,cookie,sessionStorage,localStorage,下面這篇文章主要給大家介紹了關(guān)于localStorage設(shè)置有效期和過期時(shí)間的相關(guān)資料,需要的朋友可以參考下2022-02-02javascript 取小數(shù)點(diǎn)后幾位幾種方法總結(jié)
這篇文章主要介紹了javascript 取小數(shù)點(diǎn)后幾位幾種方法總結(jié)的相關(guān)資料,這里提供了四種方法,幫助大家整理,需要的朋友可以參考下2017-08-08JavaScript前端實(shí)現(xiàn)拼圖分割效果
這篇文章主要為大家詳細(xì)介紹了如何通過JavaScript前端實(shí)現(xiàn)拼圖分割效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-10-10JavaScript中的淺拷貝和深拷貝原理與實(shí)現(xiàn)淺析
這篇文章主要介紹了JavaScript中的淺拷貝和深拷貝原理與實(shí)現(xiàn),JavaScript 中的淺拷貝和深拷貝指的是在復(fù)制對(duì)象(包括對(duì)象、數(shù)組等)時(shí),是否只復(fù)制對(duì)象的引用地址或者在復(fù)制時(shí)創(chuàng)建一個(gè)新的對(duì)象2023-04-04JavaScript遍歷table表格中的某行某列并打印其值
這篇文章主要介紹了JavaScript遍歷table表格中的某行某列并打印其值,需要的朋友可以參考下2014-07-07Popup彈出框添加數(shù)據(jù)實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了Popup彈出框添加數(shù)據(jù)的簡單實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10js ondocumentready onmouseover onclick onmouseout 樣式
下面都是一些上面的事件觸發(fā)的事先定義的代碼。2010-07-07