前端實現(xiàn)無感刷新Token的方法與避坑指南
刷新 Token 不是“過期就重新登錄”,而是讓用戶毫無感知地繼續(xù)使用。
可惜,大多數(shù)項目還在用 401 跳登錄 粗暴處理——這根本不是用戶體驗,這是放棄治療。
在現(xiàn)代 Web 應(yīng)用中,用戶登錄后通常會獲得一對 Token:
- Access Token(短期有效,如 15 分鐘)
- Refresh Token(長期有效,如 7 天)
當(dāng) Access Token 過期時,理想狀態(tài)是:前端自動用 Refresh Token 換取新 Token,并重試原請求——整個過程用戶無感,頁面不跳轉(zhuǎn)、操作不中斷。
但現(xiàn)實呢?
“Token 過期 → 彈出登錄框 → 用戶罵一句‘怎么又登出了’ → 關(guān)掉頁面走人。”
今天,我們就來徹底搞懂:如何真正實現(xiàn)“無感刷新”Token?為什么 90% 的實現(xiàn)都有致命缺陷?
錯誤做法一:在每個接口里手動判斷 401
// 千萬別這么寫!
fetch('/api/user')
.then(res => {
if (res.status === 401) {
// 重新登錄 or 刷新 token?
window.location.href = '/login';
}
});問題在哪?
- 每個接口都要重復(fù)寫邏輯;
- 如果多個請求同時 401,會觸發(fā)多次刷新,甚至多次跳登錄;
- 完全無法做到“無感”。
錯誤做法二:全局?jǐn)r截 401 后直接刷新 Token 并重試一次
這是目前最“主流”的錯誤方案:
// 偽代碼:看似聰明,實則危險
axios.interceptors.response.use(
res => res,
async (error) => {
if (error.response.status === 401) {
const newToken = await refreshToken(); // 獲取新 token
saveToken(newToken);
// 用新 token 重試原請求
return axios(error.config);
}
}
);
表面看沒問題,但隱藏三大坑
坑 1:并發(fā)請求雪崩
當(dāng)頁面剛加載,10 個接口同時發(fā)起,而此時 Token 已過期 ——10 個請求全部返回 401 → 觸發(fā) 10 次 refreshToken() → 后端收到 10 個刷新請求!
后果:
- 后端可能拒絕重復(fù)刷新(安全策略);
- Refresh Token 被提前消耗,后續(xù)真失效;
- 用戶反而被踢下線。
坑 2:Refresh Token 泄露風(fēng)險
如果前端把 Refresh Token 存在 localStorage,一旦 XSS 攻擊成功,攻擊者可長期盜用賬號。
安全最佳實踐:Refresh Token 應(yīng)僅存于 HttpOnly Cookie,前端不可讀!
但上述方案要求前端“拿到新 token”,這就逼你把 Refresh Token 暴露給 JS —— 安全與功能不可兼得?
坑 3:無限重試死循環(huán)
如果 refreshToken() 本身也返回 401(比如 Refresh Token 也過期了),重試原請求 → 又 401 → 再刷新 → 再 401 → ……
瀏覽器卡死,內(nèi)存飆升。
正確方式:用“鎖機制 + 隊列 + 安全存儲”三位一體
要實現(xiàn)真正的無感刷新,必須同時解決:
- 并發(fā)控制(只刷一次)
- 安全存儲(Refresh Token 不暴露給 JS)
- 失敗兜底(Refresh 失敗時優(yōu)雅降級)
第一步:后端配合 —— Refresh Token 存 HttpOnly Cookie
HTTP/1.1 200 OK
Set-Cookie: refreshToken=abc123; HttpOnly; Secure; SameSite=Strict; Path=/auth
前端永遠拿不到 refreshToken,但每次請求會自動攜帶。
第二步:前端實現(xiàn)“單例刷新鎖 + 請求隊列”
let isRefreshing = false;
let refreshPromise = null;
const failedQueue = [];
// 重試隊列中的請求
const processQueue = (error, token = null) => {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error);
} else {
resolve(token);
}
});
failedQueue.length = 0;
};
axios.interceptors.response.use(
response => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// 已在刷新中,將請求加入隊列,等待新 token
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers['Authorization'] = `Bearer ${token}`;
return axios(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
// 調(diào)用刷新接口(后端從 Cookie 讀 refreshToken)
const { data } = await axios.post('/auth/refresh');
const newAccessToken = data.accessToken;
// 通知所有排隊的請求
processQueue(null, newAccessToken);
// 重試當(dāng)前請求
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// 刷新失?。呵蹇毡镜厣矸?,跳轉(zhuǎn)登錄
clearAuth();
processQueue(refreshError, null);
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
refreshPromise = null;
}
}
return Promise.reject(error);
}
);
關(guān)鍵設(shè)計解析
| 機制 | 作用 |
|---|---|
| isRefreshing 鎖 | 確保同一時間只發(fā)起一次刷新 |
| failedQueue 隊列 | 緩存所有因 401 失敗的請求,等新 token 到手后批量重試 |
| _retry 標(biāo)記 | 防止重試后的請求再次進入刷新邏輯 |
| HttpOnly Cookie | 保護 Refresh Token 不被 XSS 竊取 |
安全補充:前端 Token 存儲建議
| Token 類型 | 推薦存儲方式 | 原因 |
|---|---|---|
| Access Token | 內(nèi)存(JS 變量)或 sessionStorage | 短期有效,避免持久化泄露 |
| Refresh Token | HttpOnly Cookie | 前端不可讀,防 XSS |
切勿將任何 Token 存入 localStorage!這是 XSS 攻擊的黃金目標(biāo)。
如何測試你的刷新邏輯
- 手動將 Access Token 設(shè)為過期;
- 快速點擊多個按鈕,觸發(fā)并發(fā)請求;
- 觀察 Network 面板:
- 是否只調(diào)用了一次
/auth/refresh? - 所有原請求是否最終成功?
- 是否只調(diào)用了一次
- 模擬 Refresh Token 失效,是否跳轉(zhuǎn)登錄?
結(jié)語
“無感刷新 Token”不是炫技,而是對用戶體驗和系統(tǒng)安全的基本尊重。那些讓用戶頻繁重新登錄的產(chǎn)品,不是技術(shù)做不到,而是沒把用戶當(dāng)回事。
真正的專業(yè),藏在細節(jié)里:一個鎖、一個隊列、一個 HttpOnly Cookie —— 就是 10% 正確方案 與 90% 錯誤實現(xiàn)的分水嶺。
你的項目還在用“401 就跳登錄”嗎?是時候升級了。
歡迎轉(zhuǎn)發(fā)給那個總說“Token 過期就讓用戶重新登錄”的同事。
到此這篇關(guān)于前端實現(xiàn)無感刷新Token的方法與避坑指南的文章就介紹到這了,更多相關(guān)前端無感刷新token內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS實現(xiàn)選項卡插件的兩種寫法(jQuery和class)
這篇文章主要為大家詳細介紹了JS實現(xiàn)選項卡插件的兩種寫法:jQuery和class,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-12-12
JavaScript與DropDownList 區(qū)別分析
大家都知道,.NET中一些Web服務(wù)器控件解析并編譯,最終被渲染的時候,其實是轉(zhuǎn)化成了普通的html控件。2010-01-01
使用Webpack壓縮與轉(zhuǎn)譯JavaScript代碼的操作方法
在Web開發(fā)中,代碼的性能和加載時間是用戶體驗的重要組成部分,為此,將JavaScript代碼壓縮和優(yōu)化是發(fā)布前一個必不可少的步驟,所以本文給大家介紹了如何使用Webpack壓縮與轉(zhuǎn)譯JavaScript代碼,需要的朋友可以參考下2024-05-05
innerHTML動態(tài)添加html代碼和腳本兼容多個瀏覽器
innerHTML動態(tài)添加html代碼和腳本,給某個元素的innerHTML賦值,并使得值中的js代碼有效且兼容多個瀏覽器,很棒的一個方法2014-10-10
前端JavaScript經(jīng)典之Promise詳解
Promise是為了解決回調(diào)地獄問題而誕生的,它提供了優(yōu)雅的異步回調(diào)解決方案,這篇文章主要介紹了前端JavaScript經(jīng)典之Promise的相關(guān)資料,需要的朋友可以參考下2024-09-09

