在你的 axios 封裝文件中,你需要兩個外部變量來管理狀態:
// 標記是否正在刷新 token 的“鎖”
let isRefreshing = false;
// 存儲因 token 失效而掛起的請求的“隊列”
let requestQueue = [];
- 請求攔截器 (Request Interceptor)
它的任務很簡單:在每個請求發出去之前,都帶上當前的 token。
import axios from 'axios';
const service = axios.create({
baseURL: '/api',
timeout: 5000,
});
// 請求攔截器
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token'); // 從你的存儲中獲取 token
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
return config;
},
error => {
return Promise.reject(error);
}
);
- 響應攔截器 (Response Interceptor) - 核心邏輯所在地
這裏是處理“保安攔人”和“排隊”邏輯的地方。
// 響應攔截器
service.interceptors.response.use(
// 狀態碼為 2xx 的情況,直接返回響應
response => {
return response.data;
},
// 狀態碼非 2xx 的情況
async error => {
const { config, response } = error;
const originalRequest = config; // 獲取原始請求配置
// 1. 判斷是否是 401 (未授權) 錯誤
if (response && response.status === 401) {
// 2. 判斷是否正在刷新 Token (檢查“鎖”)
if (!isRefreshing) {
// --- “第一個被攔下的人”的邏輯 ---
isRefreshing = true; // 上鎖
try {
// 3. 去“前台”辦理新卡 (調用刷新 token 的接口)
const { newToken, newRefreshToken } = await refreshTokenApi(); // 這是一個你自己封裝的刷新 token 的 API 調用
// 4. 辦理成功,更新本地存儲的 token
localStorage.setItem('token', newToken);
localStorage.setItem('refreshToken', newRefreshToken);
// 5. 把新 token 更新到當前 axios 實例的默認配置中,後續請求就會使用新 token
service.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
// 6. 遍歷“等待隊列”,把之前失敗的請求用新 token 重新發送一遍
requestQueue.forEach(callback => callback(newToken));
// 清空隊列
requestQueue = [];
// 7. 把自己這個最初失敗的請求也重新發送一遍
return service(originalRequest);
} catch (refreshError) {
// 如果刷新 token 本身也失敗了(比如 refresh token 也過期了)
// 就跳轉到登錄頁,並清空所有狀態
console.error('Unable to refresh token.', refreshError);
// 清理本地存儲、跳轉登錄頁等操作
// redirectToLogin();
return Promise.reject(refreshError);
} finally {
// 解鎖
isRefreshing = false;
}
} else {
// --- “後面排隊的人”的邏輯 ---
// 如果正在刷新 token,就把當前失敗的請求包裝成一個 Promise,存入隊列
return new Promise((resolve) => {
// 這個 callback 的參數就是未來獲取到的新 token
requestQueue.push((token) => {
// 當拿到新 token 後,更新這個請求的 header,然後重新發起請求
originalRequest.headers['Authorization'] = 'Bearer ' + token;
resolve(service(originalRequest));
});
});
}
}
// 如果不是 401 錯誤,直接把錯誤拋出
return Promise.reject(error);
}
);
// 模擬一個刷新 token 的 API
async function refreshTokenApi() {
const refreshToken = localStorage.getItem('refreshToken');
// 實際項目中,這裏會用 axios 發送一個請求
return new Promise(resolve => {
setTimeout(() => {
console.log('Token refreshed!');
resolve({
newToken: 'a-brand-new-token-string',
newRefreshToken: 'a-brand-new-refresh-token-string',
});
}, 1000);
});
}
export default service;
流程圖
為了讓你更清晰地理解,這裏有一個流程圖:
graph TD
A[發起多個API請求] --> B{響應攔截器};
B --> C{狀態碼是 401?};
C -- 否 --> D[請求成功/其他錯誤];
C -- 是 --> E{isRefreshing === false? (我是第一個嗎?)};
E -- 是 --> F[isRefreshing = true (上鎖)];
F --> G[調用 refreshToken API];
G -- 成功 --> H[更新本地和axios的Token];
H --> I[執行隊列裏的所有請求];
I --> J[重試我自己的請求];
J --> K[isRefreshing = false (解鎖)];
G -- 失敗 --> L[跳轉到登錄頁];
L --> K;
E -- 否 --> M[當前請求進入等待隊列];
M --> N[等待...];
I --> N;
D --> Z[結束];
K --> Z;
ps: 文檔來源 google AI
核心點:
1、同一個頁面發送的所有ajax他們共享很多變量
比如isRefreshing 、requestQueue,並不是發送一個接口就實例化一次變量
2、一旦函數內部寫了return後,就不會執行任何函數後面的代碼了
所以需要把 請求自己那段代碼寫在最後。
// 7. 把自己這個最初失敗的請求也重新發送一遍
return service(originalRequest);
從上面的請求順序來看,去發起刷新token的那個原始接口,在拿到最新token後,先執行了後面排隊的那些請求,執行完了後,自己最後才去請求的。