From c54a7f4f36017d240c671a927c672c43ae5afb58 Mon Sep 17 00:00:00 2001 From: sunlei Date: Sun, 17 May 2026 15:32:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E4=BF=AE=E5=A4=8D=20401=20?= =?UTF-8?q?=E9=89=B4=E6=9D=83=E6=81=A2=E5=A4=8D=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth.ts | 4 +++ src/api/request.ts | 72 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index bf751db..c1867fb 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -18,6 +18,7 @@ const ACCESS_CODES_KEY = "kt-admin-access-codes"; const USER_INFO_KEY = "kt-admin-user-info"; let refreshPromise: Promise | null = null; +let redirectingToAdminLogin = false; const authClient = axios.create({ baseURL: config.axiosBase, @@ -66,6 +67,9 @@ const buildAdminLoginUrl = (redirect: string) => { }; export const redirectToAdminLogin = () => { + if (redirectingToAdminLogin) return; + + redirectingToAdminLogin = true; window.location.href = buildAdminLoginUrl(window.location.href); }; diff --git a/src/api/request.ts b/src/api/request.ts index b4c285f..e62c9d9 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -10,9 +10,18 @@ import { export interface ApiResponse { code: number; data: T; + message?: string; msg: string; } +type AuthRetryConfig = AxiosRequestConfig & { + _authRetried?: boolean; +}; + +type AuthHeaderMap = Record & { + get?: (name: string) => unknown; +}; + const request = axios.create({ baseURL: config.axiosBase, timeout: 1000 * 30, @@ -27,6 +36,49 @@ export const post = (url: string, data?: any, config?: AxiosRequestConf return request.post>(url, data, config); }; +const getAuthErrorMessage = (data?: Partial) => { + return data?.msg || data?.message || "登录已过期"; +}; + +const getRequestAuthorization = (requestConfig?: AuthRetryConfig) => { + const headers = requestConfig?.headers as AuthHeaderMap | undefined; + + if (!headers) return null; + if (typeof headers.get === "function") { + return headers.get("Authorization") || headers.get("authorization"); + } + + return headers.Authorization || headers.authorization; +}; + +const retryRequestWithFreshToken = async (requestConfig?: AuthRetryConfig) => { + if (!requestConfig || requestConfig._authRetried) return null; + + const hasOldAccessToken = Boolean( + getStoredAccessToken() || getRequestAuthorization(requestConfig), + ); + if (!hasOldAccessToken) return null; + + requestConfig._authRetried = true; + clearPersistedAuth(); + const accessToken = await refreshPersistedAuth(); + + if (!accessToken) return null; + + requestConfig.headers = { + ...(requestConfig.headers || {}), + Authorization: `Bearer ${accessToken}`, + }; + + // 只有旧 accessToken 过期时才尝试刷新并重放一次,未登录 401 直接去 Admin。 + return request.request(requestConfig); +}; + +const redirectAfterAuthExpired = () => { + clearPersistedAuth(); + redirectToAdminLogin(); +}; + request.interceptors.request.use(async (requestConfig) => { let accessToken = getStoredAccessToken(); @@ -42,19 +94,25 @@ request.interceptors.request.use(async (requestConfig) => { }); request.interceptors.response.use( - (response) => { + async (response) => { if (response.data?.code === 401) { - clearPersistedAuth(); - redirectToAdminLogin(); - return Promise.reject(new Error(response.data?.msg || "登录已过期")); + const retryResponse = await retryRequestWithFreshToken(response.config as AuthRetryConfig); + if (retryResponse) return retryResponse; + + redirectAfterAuthExpired(); + return Promise.reject(new Error(getAuthErrorMessage(response.data))); } return response.data; }, - (error) => { + async (error) => { if (axios.isAxiosError(error) && error.response?.status === 401) { - clearPersistedAuth(); - redirectToAdminLogin(); + const retryResponse = await retryRequestWithFreshToken( + error.config as AuthRetryConfig | undefined, + ); + if (retryResponse) return retryResponse; + + redirectAfterAuthExpired(); } return Promise.reject(error);