163 lines
4.6 KiB
TypeScript
163 lines
4.6 KiB
TypeScript
import axios, { type AxiosRequestConfig } from "axios";
|
|
|
|
// ============================================================
|
|
// Token 存储 Key
|
|
// ============================================================
|
|
|
|
const ACCESS_TOKEN_KEY = "track_admin_token";
|
|
const REFRESH_TOKEN_KEY = "track_admin_refresh_token";
|
|
const USER_KEY = "track_admin_user";
|
|
|
|
// ============================================================
|
|
// Token 读写工具
|
|
// ============================================================
|
|
|
|
export function getAccessToken(): string | null {
|
|
return localStorage.getItem(ACCESS_TOKEN_KEY);
|
|
}
|
|
|
|
export function getRefreshToken(): string | null {
|
|
return localStorage.getItem(REFRESH_TOKEN_KEY);
|
|
}
|
|
|
|
export function setTokens(accessToken: string, refreshToken: string) {
|
|
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
|
|
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
|
}
|
|
|
|
export function clearAuth() {
|
|
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
|
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
|
localStorage.removeItem(USER_KEY);
|
|
}
|
|
|
|
// ============================================================
|
|
// Axios 实例
|
|
// ============================================================
|
|
|
|
const api = axios.create({
|
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
timeout: 10000,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
|
|
// ============================================================
|
|
// 请求拦截器 — 注入 Access Token
|
|
// ============================================================
|
|
|
|
api.interceptors.request.use(
|
|
(config) => {
|
|
const token = getAccessToken();
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
},
|
|
(error) => Promise.reject(error),
|
|
);
|
|
|
|
// ============================================================
|
|
// 响应拦截器 — 双 Token 无感刷新 + 并发请求队列
|
|
// ============================================================
|
|
|
|
let isRefreshing = false;
|
|
let retryQueue: Array<{
|
|
resolve: (token: string) => void;
|
|
reject: (error: unknown) => void;
|
|
}> = [];
|
|
|
|
/** 处理队列中的所有挂起请求 */
|
|
function processQueue(error: unknown, token: string | null) {
|
|
retryQueue.forEach((p) => {
|
|
if (token) {
|
|
p.resolve(token);
|
|
} else {
|
|
p.reject(error);
|
|
}
|
|
});
|
|
retryQueue = [];
|
|
}
|
|
|
|
api.interceptors.response.use(
|
|
(response) => response,
|
|
async (error) => {
|
|
const originalRequest: AxiosRequestConfig & { _retry?: boolean } =
|
|
error.config;
|
|
const status = error.response?.status;
|
|
|
|
// 仅处理 401
|
|
if (status !== 401) {
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
// 跳过登录和刷新接口自身(避免死循环)
|
|
const url = originalRequest.url ?? "";
|
|
if (url.includes("/auth/login") || url.includes("/auth/refresh")) {
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
// 避免对同一请求重复刷新
|
|
if (originalRequest._retry) {
|
|
clearAuth();
|
|
if (window.location.pathname.startsWith("/admin")) {
|
|
window.location.href = "/admin/login";
|
|
}
|
|
return Promise.reject(error);
|
|
}
|
|
originalRequest._retry = true;
|
|
|
|
// ---- 并发请求队列机制 ----
|
|
if (isRefreshing) {
|
|
// 已有刷新进行中,加入队列等待
|
|
return new Promise<string>((resolve, reject) => {
|
|
retryQueue.push({ resolve, reject });
|
|
}).then((newToken) => {
|
|
originalRequest.headers = originalRequest.headers || {};
|
|
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
return api(originalRequest);
|
|
});
|
|
}
|
|
|
|
isRefreshing = true;
|
|
|
|
try {
|
|
const refreshToken = getRefreshToken();
|
|
if (!refreshToken) {
|
|
throw new Error("无 Refresh Token");
|
|
}
|
|
|
|
// 调用刷新接口
|
|
const { data } = await axios.post(
|
|
`${import.meta.env.VITE_API_BASE_URL}/auth/refresh`,
|
|
{ refresh_token: refreshToken },
|
|
{ headers: { "Content-Type": "application/json" } },
|
|
);
|
|
|
|
const newAccessToken: string = data.access_token;
|
|
setTokens(newAccessToken, refreshToken); // 更新 Access Token
|
|
|
|
// 重放队列中的所有请求
|
|
processQueue(null, newAccessToken);
|
|
|
|
// 重试当前请求
|
|
originalRequest.headers = originalRequest.headers || {};
|
|
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
|
return api(originalRequest);
|
|
} catch (refreshError) {
|
|
// Refresh Token 也过期 — 彻底登出
|
|
processQueue(refreshError, null);
|
|
clearAuth();
|
|
if (window.location.pathname.startsWith("/admin")) {
|
|
window.location.href = "/admin/login";
|
|
}
|
|
return Promise.reject(refreshError);
|
|
} finally {
|
|
isRefreshing = false;
|
|
}
|
|
},
|
|
);
|
|
|
|
export default api;
|