2026-02-10 09:48:44 +00:00
|
|
|
|
import axios from "axios";
|
2026-02-26 09:09:14 +00:00
|
|
|
|
import { message } from "antd";
|
2026-02-10 09:48:44 +00:00
|
|
|
|
|
2026-05-11 02:54:33 +00:00
|
|
|
|
declare module "axios" {
|
|
|
|
|
|
interface AxiosRequestConfig {
|
|
|
|
|
|
suppressErrorToast?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface InternalAxiosRequestConfig {
|
|
|
|
|
|
suppressErrorToast?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 09:48:44 +00:00
|
|
|
|
const http = axios.create({
|
2026-03-17 07:31:09 +00:00
|
|
|
|
baseURL: "/",
|
2026-02-10 09:48:44 +00:00
|
|
|
|
timeout: 15000
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-08 11:45:50 +00:00
|
|
|
|
const refreshClient = axios.create({
|
|
|
|
|
|
baseURL: "/",
|
|
|
|
|
|
timeout: 15000
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const AUTH_WHITELIST = ["/sys/auth/login", "/sys/auth/refresh", "/sys/auth/captcha", "/sys/auth/device-code"];
|
|
|
|
|
|
const API_SUCCESS_CODE = "200";
|
|
|
|
|
|
const REFRESH_AHEAD_MS = 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
let refreshPromise: Promise<string | null> | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
function isApiSuccessCode(code: unknown): boolean {
|
|
|
|
|
|
return String(code) === API_SUCCESS_CODE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTokenPayload(token: string): Record<string, any> | 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<string | null> {
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 09:48:44 +00:00
|
|
|
|
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;
|
2026-04-08 11:45:50 +00:00
|
|
|
|
if (body && !isApiSuccessCode(body.code)) {
|
2026-02-26 09:09:14 +00:00
|
|
|
|
const errorMsg = body.msg || "请求失败";
|
2026-05-11 02:54:33 +00:00
|
|
|
|
if (!resp.config?.suppressErrorToast) {
|
|
|
|
|
|
message.error(errorMsg);
|
|
|
|
|
|
}
|
2026-02-26 09:09:14 +00:00
|
|
|
|
const err = new Error(errorMsg);
|
2026-02-26 05:53:58 +00:00
|
|
|
|
(err as any).code = body.code;
|
|
|
|
|
|
(err as any).msg = body.msg;
|
|
|
|
|
|
return Promise.reject(err);
|
2026-02-10 09:48:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
return resp;
|
|
|
|
|
|
},
|
2026-04-08 11:45:50 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 05:43:00 +00:00
|
|
|
|
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
|
2026-04-08 11:45:50 +00:00
|
|
|
|
clearAuthStorage();
|
2026-02-25 05:43:00 +00:00
|
|
|
|
window.location.href = "/login?timeout=1";
|
2026-02-26 09:09:14 +00:00
|
|
|
|
return Promise.reject(error);
|
2026-02-12 07:51:03 +00:00
|
|
|
|
}
|
2026-04-08 11:45:50 +00:00
|
|
|
|
|
2026-02-26 05:53:58 +00:00
|
|
|
|
const body = error.response?.data;
|
2026-02-26 09:09:14 +00:00
|
|
|
|
const errorMsg = body?.msg || error.message || "网络异常";
|
2026-05-11 02:54:33 +00:00
|
|
|
|
if (!originalRequest.suppressErrorToast) {
|
|
|
|
|
|
message.error(errorMsg);
|
|
|
|
|
|
}
|
2026-02-26 09:09:14 +00:00
|
|
|
|
|
2026-02-26 05:53:58 +00:00
|
|
|
|
if (body && body.msg) {
|
2026-04-08 11:45:50 +00:00
|
|
|
|
const err = new Error(body.msg);
|
|
|
|
|
|
(err as any).code = body.code;
|
|
|
|
|
|
(err as any).msg = body.msg;
|
|
|
|
|
|
return Promise.reject(err);
|
2026-02-26 05:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 07:51:03 +00:00
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
|
}
|
2026-02-10 09:48:44 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
export default http;
|