import axios from "axios"; import { message } from "antd"; import { buildLoginRedirect, clearAuth, getAccessToken, getRefreshToken, saveTokens } from "@/utils/auth"; declare module "axios" { interface AxiosRequestConfig { suppressErrorToast?: boolean; } interface InternalAxiosRequestConfig { suppressErrorToast?: boolean; _retry?: boolean; } } 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 SUCCESS_CODE = "200"; const REFRESH_AHEAD_MS = 60 * 1000; const http = axios.create({ baseURL: "/", timeout: 15000, }); const refreshClient = axios.create({ baseURL: "/", timeout: 15000, }); let refreshPromise: Promise | null = null; function isSuccessCode(code: unknown) { return String(code) === SUCCESS_CODE; } function isAuthWhitelistRequest(url?: string) { return AUTH_WHITELIST.some((path) => (url || "").includes(path)); } function getTokenExpireAt(token: string): number | null { try { const payload = token.split(".")[1]; if (!payload) { return null; } const normalized = payload.replace(/-/g, "+").replace(/_/g, "/"); const parsed = JSON.parse(decodeURIComponent(escape(window.atob(normalized)))); return typeof parsed.exp === "number" ? parsed.exp * 1000 : null; } catch { return null; } } function isTokenExpiringSoon(token: string) { const expireAt = getTokenExpireAt(token); if (!expireAt) { return false; } return expireAt - Date.now() <= REFRESH_AHEAD_MS; } async function refreshAccessToken(): Promise { if (refreshPromise) { return refreshPromise; } const refreshToken = getRefreshToken(); if (!refreshToken) { return null; } refreshPromise = refreshClient .post("/sys/auth/refresh", { refreshToken }) .then((resp) => { const body = resp.data; if (!body || !isSuccessCode(body.code) || !body.data?.accessToken || !body.data?.refreshToken) { throw new Error(body?.msg || "刷新登录态失败"); } saveTokens(body.data.accessToken, body.data.refreshToken); return body.data.accessToken as string; }) .catch(() => { clearAuth(); return null; }) .finally(() => { refreshPromise = null; }); return refreshPromise; } http.interceptors.request.use(async (config) => { if (!isAuthWhitelistRequest(config.url)) { const currentToken = getAccessToken(); if (currentToken && isTokenExpiringSoon(currentToken)) { await refreshAccessToken(); } } const token = getAccessToken(); if (token) { config.headers = config.headers || {}; config.headers.Authorization = `Bearer ${token}`; } return config; }); http.interceptors.response.use( (resp) => { const body = resp.data; if (body && !isSuccessCode(body.code)) { const errorMsg = body.msg || "请求失败"; if (!resp.config?.suppressErrorToast) { message.error(errorMsg); } const err = new Error(errorMsg); (err as Error & { code?: string }).code = body.code; 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)) { clearAuth(); window.location.href = buildLoginRedirect(); return Promise.reject(error); } const errorMsg = error.response?.data?.msg || error.message || "网络异常"; if (!originalRequest.suppressErrorToast) { message.error(errorMsg); } return Promise.reject(error); }, ); export default http;