diff --git a/src/api/auth.ts b/src/api/auth.ts index 543145d..2f96fce 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -17,6 +17,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 function getApiBase() { return import.meta.env.VITE_APP_API_BASE || '/api' @@ -76,6 +77,9 @@ function buildAdminLoginUrl(redirect: string) { } export function 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 4f30d7a..a473579 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -8,10 +8,19 @@ import { export type ApiResponse = { code: number + message?: string msg: string data: T } +type AuthRetryConfig = AxiosRequestConfig & { + _authRetried?: boolean +} + +type AuthHeaderMap = Record & { + get?: (name: string) => unknown +} + const request = axios.create({ baseURL: import.meta.env.VITE_APP_API_BASE || '/api', timeout: 1000 * 30, @@ -40,14 +49,61 @@ request.interceptors.request.use(async (config) => { return config }) +function getAuthErrorMessage(data?: Partial) { + return data?.msg || data?.message || '登录已过期' +} + +function getRequestAuthorization(config?: AuthRetryConfig) { + const headers = config?.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 +} + +async function retryRequestWithFreshToken(config?: AuthRetryConfig) { + if (!config || config._authRetried) return null + + const hasOldAccessToken = Boolean( + getStoredAccessToken() || getRequestAuthorization(config), + ) + if (!hasOldAccessToken) return null + + config._authRetried = true + clearPersistedAuth() + const accessToken = await refreshPersistedAuth() + + if (!accessToken) return null + + config.headers = { + ...(config.headers || {}), + Authorization: `Bearer ${accessToken}`, + } + + // 只有旧 accessToken 过期时才尝试刷新并重放一次,未登录 401 直接去 Admin。 + return request.request(config) +} + +function redirectAfterAuthExpired() { + clearPersistedAuth() + redirectToAdminLogin() +} + request.interceptors.response.use( - (response) => { + async (response) => { const data = response.data as ApiResponse if (response.status === 401 || data.code === 401) { - clearPersistedAuth() - redirectToAdminLogin() - return Promise.reject(new Error(data.msg || '登录已过期')) + const retryResponse = await retryRequestWithFreshToken( + response.config as AuthRetryConfig, + ) + if (retryResponse) return retryResponse + + redirectAfterAuthExpired() + return Promise.reject(new Error(getAuthErrorMessage(data))) } if (data.code !== 200) { @@ -56,15 +112,24 @@ request.interceptors.response.use( return data.data as any }, - (error) => { + async (error) => { if (axios.isAxiosError(error)) { if (error.response?.status === 401) { - clearPersistedAuth() - redirectToAdminLogin() + const retryResponse = await retryRequestWithFreshToken( + error.config as AuthRetryConfig | undefined, + ) + if (retryResponse) return retryResponse + + redirectAfterAuthExpired() } return Promise.reject( - new Error(error.response?.data?.msg || error.message || '请求失败'), + new Error( + error.response?.data?.msg || + error.response?.data?.message || + error.message || + '请求失败', + ), ) }