import axios from "axios"; import { message } from "antd"; declare module "axios" { interface AxiosRequestConfig { suppressErrorToast?: boolean; } interface InternalAxiosRequestConfig { suppressErrorToast?: boolean; } } const http = axios.create({ baseURL: "/", timeout: 15000 }); const refreshClient = axios.create({ baseURL: "/", timeout: 15000 }); const AUTH_WHITELIST = [ "/sys/auth/login", "/sys/auth/refresh", "/sys/auth/captcha", "/sys/auth/device-code", "/sys/auth/password-policy/public", "/sys/auth/password-recovery/send-code", "/sys/auth/password-recovery/reset" ]; const API_SUCCESS_CODE = "200"; const REFRESH_AHEAD_MS = 60 * 1000; let refreshPromise: Promise | null = null; function isApiSuccessCode(code: unknown): boolean { return String(code) === API_SUCCESS_CODE; } function getTokenPayload(token: string): Record | null { try { const payload = token.split(".")[1]; if (!payload) { return null; } const normalized = payload.replace(/-/g, "+").replace(/_/g, "/"); return JSON.parse(decodeURIComponent(escape(window.atob(normalized)))); } catch { return null; } } function getTokenExpireAt(token: string): number | null { const payload = getTokenPayload(token); if (!payload || typeof payload.exp !== "number") { return null; } return payload.exp * 1000; } function isTokenExpiringSoon(token: string): boolean { const expireAt = getTokenExpireAt(token); if (!expireAt) { return false; } return expireAt - Date.now() <= REFRESH_AHEAD_MS; } function clearAuthStorage() { localStorage.removeItem("accessToken"); localStorage.removeItem("refreshToken"); sessionStorage.removeItem("userProfile"); } function persistTokens(data: { accessToken: string; refreshToken: string }) { localStorage.setItem("accessToken", data.accessToken); localStorage.setItem("refreshToken", data.refreshToken); const payload = getTokenPayload(data.accessToken); if (payload && payload.tenantId !== undefined && payload.tenantId !== null) { localStorage.setItem("activeTenantId", String(payload.tenantId)); } } function isAuthWhitelistRequest(url?: string) { return AUTH_WHITELIST.some((path) => (url || "").includes(path)); } async function refreshAccessToken(): Promise { if (refreshPromise) { return refreshPromise; } const refreshToken = localStorage.getItem("refreshToken"); if (!refreshToken) { return null; } refreshPromise = refreshClient .post("/sys/auth/refresh", { refreshToken }) .then((resp) => { const body = resp.data; if (!body || !isApiSuccessCode(body.code) || !body.data?.accessToken || !body.data?.refreshToken) { throw new Error(body?.msg || "刷新登录态失败"); } persistTokens(body.data); return body.data.accessToken as string; }) .catch(() => { clearAuthStorage(); return null; }) .finally(() => { refreshPromise = null; }); return refreshPromise; } http.interceptors.request.use(async (config) => { if (!isAuthWhitelistRequest(config.url)) { const currentToken = localStorage.getItem("accessToken"); if (currentToken && isTokenExpiringSoon(currentToken)) { await refreshAccessToken(); } } const token = localStorage.getItem("accessToken"); if (token) { config.headers = config.headers || {}; config.headers.Authorization = `Bearer ${token}`; } return config; }); http.interceptors.response.use( (resp) => { const body = resp.data; if (body && !isApiSuccessCode(body.code)) { const errorMsg = body.msg || "请求失败"; if (!resp.config?.suppressErrorToast) { message.error(errorMsg); } const err = new Error(errorMsg); (err as any).code = body.code; (err as any).msg = body.msg; return Promise.reject(err); } return resp; }, async (error) => { const originalRequest = error.config || {}; if ( error.response?.status === 401 && !originalRequest._retry && !isAuthWhitelistRequest(originalRequest.url) ) { originalRequest._retry = true; const newToken = await refreshAccessToken(); if (newToken) { originalRequest.headers = originalRequest.headers || {}; originalRequest.headers.Authorization = `Bearer ${newToken}`; return http(originalRequest); } } if (error.response && (error.response.status === 401 || error.response.status === 403)) { clearAuthStorage(); window.location.href = "/login?timeout=1"; return Promise.reject(error); } const body = error.response?.data; const errorMsg = body?.msg || error.message || "网络异常"; if (!originalRequest.suppressErrorToast) { message.error(errorMsg); } if (body && body.msg) { const err = new Error(body.msg); (err as any).code = body.code; (err as any).msg = body.msg; return Promise.reject(err); } return Promise.reject(error); } ); export default http;