沒有什么比在用戶操作得正嗨時,突然提示“登錄已過期,請重新登錄”的提示更讓人沮喪的了。這種突兀的中斷不僅破壞了用戶體驗,甚至可能導(dǎo)致未保存的數(shù)據(jù)丟失。
然而,我們都知道,出于安全考慮,用于身份驗證的 Token
(通常是 Access Token
)必須有較短的有效期。那么,我們?nèi)绾卧诒WC安全的前提下,創(chuàng)造一種“永不掉線”的絲滑體驗?zāi)兀?/span>
問題的根源:Access Token 的“天生矛盾”
首先,我們要理解為什么需要刷新 Token。
我們通常使用 Access Token
來驗證用戶的每一次 API 請求。為了安全,Access Token
的生命周期被設(shè)計得很短(例如 30 分鐘或 1 小時)。如果有效期太長,一旦泄露,攻擊者就能在很長一段時間內(nèi)冒充用戶進(jìn)行操作,風(fēng)險極高。
這就產(chǎn)生了一個矛盾:
- 安全性要求:
Access Token
有效期要短。 - 用戶體驗要求:用戶不想頻繁地被強(qiáng)制重新登錄。
為了解決這個矛盾,Refresh Token
應(yīng)運(yùn)而生。
核心理念:雙 Token 認(rèn)證系統(tǒng)
無感刷新機(jī)制的核心在于引入了兩種類型的 Token:
Access Token
(訪問令牌)
- 用途:用于訪問受保護(hù)的 API 資源,附加在每個請求的
Header
中。 - 特點(diǎn):生命周期短(如 1 小時),無狀態(tài),服務(wù)器無需存儲。
- 存儲:通常存儲在客戶端內(nèi)存中(如 Vuex/Redux),因為需要頻繁讀取。
Refresh Token
(刷新令牌)
- 用途:當(dāng)
Access Token
過期時,專門用于獲取一個新的 Access Token
。 - 特點(diǎn):生命周期長(如 7 天或 30 天),與特定用戶綁定,服務(wù)器需要安全存儲其有效性記錄。
- 存儲:必須安全存儲。最佳實踐是存儲在
HttpOnly
Cookie 中,這樣可以防止客戶端 JavaScript 腳本(如 XSS 攻擊)讀取它。
既然如此,為何不直接使用 Refresh Token
呢?
Access Token
通常是無狀態(tài)的,服務(wù)器無需記錄它,也導(dǎo)致 JWT 無法主動吊銷,而 Refresh Token
是有狀態(tài)的,服務(wù)器需要一個列表(數(shù)據(jù)庫中的“白名單”或“吊銷列表”)來記錄哪些 Refresh Token
是有效的,當(dāng)用戶更改密碼、或從某個設(shè)備上“主動登出”時,服務(wù)器端可以主動將對應(yīng)的 Refresh Token
設(shè)為無效。
無感刷新的詳細(xì)工作流
下面是這個“魔法”發(fā)生的具體步驟:
- 首次登錄:用戶使用用戶名和密碼登錄。服務(wù)器驗證成功后,返回一個
Access Token
和一個 Refresh Token
。 - 正常請求:客戶端將
Access Token
存儲起來,并在后續(xù)的每次 API 請求中,通過 Authorization
請求頭將其發(fā)送給服務(wù)器。 - Token 過期:當(dāng)
Access Token
過期后,客戶端再次用它請求 API。服務(wù)器會拒絕該請求,并返回一個特定的狀態(tài)碼,通常是 401 Unauthorized
。 - 攔截 401 錯誤:客戶端的請求層(如 Axios 攔截器)會捕獲這個
401
錯誤。此時,它不會立即通知用戶“你已掉線”,而是暫停這個失敗的請求。 - 發(fā)起刷新請求:攔截器使用
Refresh Token
去調(diào)用一個專門的刷新接口(例如 /api/auth/refresh
)。 - 處理刷新結(jié)果:
- 刷新成功:服務(wù)器驗證
Refresh Token
有效,生成一個新的 Access Token
(有時也會返回一個新的 Refresh Token
,這被稱為“刷新令牌旋轉(zhuǎn)”策略,可以提高安全性),并將其返回給客戶端。 - 刷新失敗:如果
Refresh Token
也過期了或無效,服務(wù)器會返回錯誤(如 403 Forbidden
)。這意味著用戶的登錄會話徹底結(jié)束。
- 重試與終結(jié):
- 若刷新成功:客戶端用新的
Access Token
自動重發(fā)剛才失敗的那個 API 請求。用戶完全感覺不到任何中斷,數(shù)據(jù)操作無縫銜接。 - 若刷新失敗:客戶端清除所有認(rèn)證信息,強(qiáng)制用戶登出,并重定向到登錄頁面。
實戰(zhàn)演練:使用 Axios 攔截器實現(xiàn)無感刷新
Axios
的攔截器是實現(xiàn)這一流程的完美工具。下面是一個完整且考慮了并發(fā)問題的實現(xiàn)方案。
1. 創(chuàng)建 Axios 實例
首先,我們創(chuàng)建一個單獨(dú)的 Axios 實例,方便統(tǒng)一管理。
// a-pi/request.js
import axios from 'axios';
const service = axios.create({
baseURL: '/api',
timeout: 10000,
});
// 請求攔截器
service.interceptors.request.use(
config => {
// 在發(fā)送請求之前,從 state management (e.g., Vuex/Pinia/Redux) 獲取 token
const accessToken = getAccessTokenFromStore();
if (accessToken) {
config.headers['Authorization'] = `Bearer ${accessToken}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
2. 核心:響應(yīng)攔截器
這是實現(xiàn)無感刷新的關(guān)鍵。
// a-pi/request.js (續(xù))
// 用于刷新 token 的 API
import { refreshTokenApi } from './auth';
let isRefreshing = false; // 控制刷新狀態(tài)的標(biāo)志
let requests = []; // 存儲因 token 過期而掛起的請求
service.interceptors.response.use(
response => response, // 對成功響應(yīng)直接返回
async error => {
const { config, response: { status } } = error;
// 1. 如果不是 401 錯誤,直接返回錯誤
if (status !== 401) {
return Promise.reject(error);
}
// 2. 避免重復(fù)刷新:如果正在刷新 token,將后續(xù)請求暫存
if (isRefreshing) {
return new Promise(resolve => {
requests.push(() => resolve(service(config)));
});
}
isRefreshing = true;
try {
// 3. 調(diào)用刷新 token 的 API
const { newAccessToken } = await refreshTokenApi(); // 假設(shè) refresh token 通過 HttpOnly cookie 自動發(fā)送
// 4. 更新本地存儲的 access token
setAccessTokenInStore(newAccessToken);
// 5. 重試剛才失敗的請求
config.headers['Authorization'] = `Bearer ${newAccessToken}`;
// 6. 重新執(zhí)行所有被掛起的請求
requests.forEach(cb => cb());
requests = []; // 清空隊列
return service(config); // 返回重試請求的結(jié)果
} catch (refreshError) {
// 7. 如果刷新 token 也失敗了,則執(zhí)行登出操作
console.error('Unable to refresh token.', refreshError);
logoutUser(); // 清除 token,重定向到登錄頁
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
);
export default service;
代碼解析:
- 并發(fā)處理:
isRefreshing
標(biāo)志和 requests
數(shù)組是關(guān)鍵。當(dāng)?shù)谝粋€ 401
錯誤觸發(fā)刷新時,isRefreshing
變?yōu)?nbsp;true
。后續(xù)在刷新完成前到達(dá)的 401
請求,都會被推進(jìn) requests
隊列中掛起,而不是重復(fù)發(fā)起刷新請求。當(dāng)刷新成功后,再遍歷隊列,依次執(zhí)行這些被掛起的請求。 - 原子操作:通過這種“加鎖”機(jī)制,確保了刷新 Token 的操作是原子的,避免了資源浪費(fèi)和潛在的競態(tài)條件。
- 優(yōu)雅降級:當(dāng)
Refresh Token
也失效時,系統(tǒng)會執(zhí)行 logoutUser()
,進(jìn)行清理工作并引導(dǎo)用戶重新登錄,這是一個優(yōu)雅的失敗處理方案。
無感刷新 Token 機(jī)制是現(xiàn)代 Web 應(yīng)用提升用戶體驗的“標(biāo)配”。它將身份驗證的復(fù)雜性隱藏在后臺,為用戶提供了一個流暢、不間斷的操作環(huán)境。
實現(xiàn)這一機(jī)制,不僅僅是寫幾行代碼,更是對認(rèn)證流程、安全性和用戶體驗三者之間平衡的深刻理解。
該文章在 2025/7/9 12:40:23 編輯過