/** * axios 实例 & JWT 拦截器。 * * - 请求拦截器:自动从 localStorage 读取 access_token 并附加 Authorization header * - 响应拦截器:遇到 401 时尝试用 refresh_token 刷新,刷新失败则清除令牌并跳转 /login * - 并发刷新保护:多个请求同时 401 时只触发一次 refresh,其余排队等待 */ import axios, { type AxiosError, type AxiosRequestConfig, type InternalAxiosRequestConfig, } from "axios"; /* ------------------------------------------------------------------ */ /* 常量 */ /* ------------------------------------------------------------------ */ const ACCESS_TOKEN_KEY = "access_token"; const REFRESH_TOKEN_KEY = "refresh_token"; /* ------------------------------------------------------------------ */ /* axios 实例 */ /* ------------------------------------------------------------------ */ export const apiClient = axios.create({ baseURL: "/api", }); /* ------------------------------------------------------------------ */ /* 请求拦截器 — 附加 JWT */ /* ------------------------------------------------------------------ */ apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => { const token = localStorage.getItem(ACCESS_TOKEN_KEY); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); /* ------------------------------------------------------------------ */ /* 响应拦截器 — 401 自动刷新 */ /* ------------------------------------------------------------------ */ /** 是否正在刷新中 */ let isRefreshing = false; /** 等待刷新完成的请求队列 */ let pendingQueue: { resolve: (token: string) => void; reject: (err: unknown) => void; }[] = []; /** 刷新完成后,依次重放排队的请求 */ function processPendingQueue(token: string | null, error: unknown) { pendingQueue.forEach(({ resolve, reject }) => { if (token) { resolve(token); } else { reject(error); } }); pendingQueue = []; } apiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as AxiosRequestConfig & { _retried?: boolean; }; // 非 401、无原始请求、或已重试过 → 直接抛出 if ( error.response?.status !== 401 || !originalRequest || originalRequest._retried ) { return Promise.reject(error); } // 刷新端点本身 401 → 不再递归刷新 if (originalRequest.url === "/auth/refresh") { clearTokensAndRedirect(); return Promise.reject(error); } // 已有刷新请求在飞 → 排队等待 if (isRefreshing) { return new Promise((resolve, reject) => { pendingQueue.push({ resolve, reject }); }).then((newToken) => { originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${newToken}`, }; originalRequest._retried = true; return apiClient(originalRequest); }); } // 发起刷新 isRefreshing = true; originalRequest._retried = true; const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY); if (!refreshToken) { isRefreshing = false; processPendingQueue(null, error); clearTokensAndRedirect(); return Promise.reject(error); } try { // 用独立 axios 调用避免被自身拦截器干扰 const { data } = await axios.post<{ access_token: string; refresh_token: string; }>("/api/auth/refresh", { refresh_token: refreshToken }); localStorage.setItem(ACCESS_TOKEN_KEY, data.access_token); localStorage.setItem(REFRESH_TOKEN_KEY, data.refresh_token); processPendingQueue(data.access_token, null); // 重放原始请求 originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${data.access_token}`, }; return apiClient(originalRequest); } catch (refreshError) { processPendingQueue(null, refreshError); clearTokensAndRedirect(); return Promise.reject(refreshError); } finally { isRefreshing = false; } }, ); /* ------------------------------------------------------------------ */ /* 辅助 */ /* ------------------------------------------------------------------ */ function clearTokensAndRedirect() { localStorage.removeItem(ACCESS_TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); // 派发自定义事件,让 authStore 监听并重置状态 // 避免直接 import authStore 导致循环依赖 window.dispatchEvent(new Event("auth:force-logout")); // 避免在登录页反复跳转 if (window.location.pathname !== "/login") { window.location.href = "/login"; } }