163 lines
4.1 KiB
TypeScript
163 lines
4.1 KiB
TypeScript
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<string | null> | 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<string | null> {
|
|
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;
|