深度拆解前端雙Token無感刷新的實(shí)現(xiàn)機(jī)制
前言
在前后端分離的項(xiàng)目中,為了安全,Token 通常會(huì)設(shè)置有效期。但如果 Token 過期時(shí)強(qiáng)制用戶重新登錄,會(huì)極大地破壞用戶體驗(yàn)。如何做到在用戶毫無察覺的情況下,自動(dòng)完成 Token 的續(xù)期?本文將深度拆解 “雙 Token 無感刷新” 的實(shí)現(xiàn)機(jī)制。
一、 為什么需要“無感刷新”?
舉個(gè)簡單例子,你正在某 App 編輯內(nèi)容,中途切出幾分鐘,再切回來時(shí),直接彈出登錄頁,提示“登錄已過期,請重新登錄”,這種場景很容易讓用戶流失。
傳統(tǒng)的單 Token 方案存在一個(gè)兩難境地:
- 有效期過短:用戶操作頻繁,動(dòng)不動(dòng)就跳回登錄頁,用戶體驗(yàn)極差。
- 有效期過長:Token 一旦被截獲,風(fēng)險(xiǎn)極高。
解決方案:雙 Token 機(jī)制
- access_token:訪問令牌。有效期短(如 1 小時(shí)),每次接口請求都攜帶,降低泄露風(fēng)險(xiǎn)。
- refresh_token:刷新令牌。有效期長(如 7 天),僅用于
access_token過期時(shí)換取新令牌。
只要用戶在 7 天內(nèi)活躍過,系統(tǒng)就能通過 refresh_token 自動(dòng)“續(xù)命”,實(shí)現(xiàn)長效無感登錄。
二、 核心流程設(shè)計(jì)
正常請求:前端攜帶 access_token 訪問。
觸發(fā)過期:后端返回 401 Unauthorized。
判斷邏輯:
- 如果是普通接口報(bào) 401:說明
access_token失效,嘗試刷新。 - 如果是刷新接口報(bào) 401:說明
refresh_token也失效了,強(qiáng)制重新登錄。
無感替換:前端自動(dòng)調(diào)用刷新接口,獲取新 Token 覆蓋本地存儲(chǔ),并重新發(fā)起之前失敗的請求。
三、 細(xì)節(jié)攻堅(jiān):如何處理并發(fā)請求?
痛點(diǎn):如果頁面同時(shí)發(fā)出了 5 個(gè)請求,而此時(shí) Token 剛好過期,會(huì)導(dǎo)致這 5 個(gè)請求同時(shí)觸發(fā)“刷新 Token”的操作,造成資源浪費(fèi)甚至后端異常。
解決策略:
- 狀態(tài)鎖 (
refreshing) :記錄當(dāng)前是否正在刷新中。 - 任務(wù)隊(duì)列 (
queue) :在刷新期間到達(dá)的請求,先暫存起來,不直接報(bào)錯(cuò)。 - 批量回放:等待 Token 刷新成功后,依次執(zhí)行隊(duì)列里的請求,實(shí)現(xiàn)“無感”銜接。
四、 代碼實(shí)現(xiàn) (Axios 攔截器)
以下是基于 Axios 的完整工程化實(shí)現(xiàn):
import axios, { AxiosRequestConfig } from 'axios';
interface PendingTask {
config: AxiosRequestConfig;
resolve: Function;
}
let refreshing = false; // 狀態(tài)鎖:標(biāo)志是否正在刷新 Token
let queue: PendingTask[] = []; // 請求隊(duì)列:暫存 Token 刷新期間的請求
const axiosInstance = axios.create({
baseURL: '/api'
});
// 1. 請求攔截器:自動(dòng)注入 Token
axiosInstance.interceptors.request.use((config) => {
const accessToken = localStorage.getItem('access_token');
if (accessToken && config.headers) {
config.headers.authorization = `Bearer ${accessToken}`;
}
return config;
});
// 2. 響應(yīng)攔截器:處理 Token 過期
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const { data, config } = error.response;
// 情況 A:正在刷新 Token 中,將后續(xù)請求存入隊(duì)列
if (refreshing) {
return new Promise((resolve) => {
queue.push({ config, resolve });
});
}
// 情況 B:access_token 過期 (狀態(tài)碼 401 且非刷新接口本身)
if (data.statusCode === 401 && !config.url.includes('/refresh')) {
refreshing = true;
try {
const res = await refreshToken();
refreshing = false;
if (res.status === 200) {
// 核心邏輯:Token 刷新成功,回放隊(duì)列中的所有請求
queue.forEach(({ config, resolve }) => {
resolve(axiosInstance(config));
});
queue = []; // 清空隊(duì)列
// 執(zhí)行當(dāng)前觸發(fā)刷新的那個(gè)請求
return axiosInstance(config);
}
} catch (err) {
refreshing = false;
queue = [];
// 情況 C:refresh_token 也過期了,徹底清除登錄態(tài)
localStorage.clear();
window.location.href = '/login';
return Promise.reject(err);
}
}
return Promise.reject(error);
}
);
/**
* 刷新 Token 的異步方法
*/
async function refreshToken() {
const res = await axios.get('/api/refresh', {
params: {
token: localStorage.getItem('refresh_token')
}
});
// 更新本地存儲(chǔ)
localStorage.setItem('access_token', res.data.accessToken);
localStorage.setItem('refresh_token', res.data.refreshToken);
return res;
}
五、 注意事項(xiàng)
- 并發(fā)請求的 Promise 掛起:在
refreshing為true時(shí),返回一個(gè)不帶resolve的new Promise是關(guān)鍵,它能讓 Axios 請求處于pending狀態(tài)。 - 錯(cuò)誤捕獲:
refreshToken接口本身報(bào)錯(cuò)(如 500 或 401)必須妥善處理,直接引導(dǎo)至登錄頁。 - 安全性:普通項(xiàng)目中可以使用
localStorage,但在更高要求的項(xiàng)目中,建議配合HttpOnly Cookie存儲(chǔ)refresh_token以防 XSS 攻擊。 - 接口重定向陷阱:確保刷新 Token 的接口不會(huì)再次進(jìn)入 401 攔截死循環(huán)。
到此這篇關(guān)于深度拆解前端雙Token無感刷新的實(shí)現(xiàn)機(jī)制的文章就介紹到這了,更多相關(guān)前端無感刷新token內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript:文字不間斷向左移動(dòng)的實(shí)例代碼
這篇文章介紹了javascript:文字不間斷向左移動(dòng)的實(shí)例代碼,有需要的朋友可以參考一下2013-08-08
JavaScript中工廠函數(shù)與構(gòu)造函數(shù)示例詳解
這篇文章主要給大家介紹了關(guān)于JavaScript中工廠函數(shù)與構(gòu)造函數(shù)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用JavaScript具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
JavaScript學(xué)習(xí)小結(jié)(7)之JS RegExp
在js中,正則表達(dá)式是由一個(gè)RegExp對象表示的,RegExp 是正則表達(dá)式的縮寫。RegExp簡單的模式可以是一個(gè)單獨(dú)的字符。更復(fù)雜的模式包括了更多的字符,并可用于解析、格式檢查、替換等等??梢允褂靡粋€(gè)RegExp()構(gòu)造函數(shù)來創(chuàng)建RegExp對象,也可以使用直接量語法2015-11-11
JavaScript如何判斷一個(gè)對象是空對象(附5種常見方法)
在JavaScript中判斷對象的類型是開發(fā)過程中的一個(gè)常見需求,尤其是在處理不確定類型的數(shù)據(jù)時(shí),這篇文章主要介紹了JavaScript如何判斷一個(gè)對象是空對象的相關(guān)資料,需要的朋友可以參考下2025-04-04
JavaScript實(shí)現(xiàn)網(wǎng)頁動(dòng)態(tài)生成表格
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)網(wǎng)頁動(dòng)態(tài)生成表格,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
微信小程序?qū)崿F(xiàn)同時(shí)上傳多張圖片
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)同時(shí)上傳多張圖片,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-02-02

