diff --git a/backend/.env.ex b/backend/.env.ex index 3740659..bf227f6 100644 --- a/backend/.env.ex +++ b/backend/.env.ex @@ -22,7 +22,7 @@ REDIS_DB=0 # 安全配置 SECRET_KEY=ex_8f7a9c3e1b4d6f2a5c8e7b9d1f3a6c4e8b2d5f7a9c1e3b6d8f2a4c7e9b1d3f5a ALGORITHM=HS256 -ACCESS_TOKEN_EXPIRE_MINUTES=30 +ACCESS_TOKEN_EXPIRE_MINUTES=480 REFRESH_TOKEN_EXPIRE_DAYS=7 # CORS配置 diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts index 601fedd..2cacb08 100644 --- a/frontend/src/api/request.ts +++ b/frontend/src/api/request.ts @@ -10,6 +10,21 @@ import { loadingManager } from '@/utils/loadingManager' // 模拟延迟,使体验更真实 const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) +// Token刷新状态管理 +let isRefreshing = false +let refreshSubscribers: Array<(token: string) => void> = [] + +// 订阅token刷新 +const subscribeTokenRefresh = (callback: (token: string) => void) => { + refreshSubscribers.push(callback) +} + +// 通知所有订阅者 +const onTokenRefreshed = (token: string) => { + refreshSubscribers.forEach(callback => callback(token)) + refreshSubscribers = [] +} + // 扩展RequestInit接口以支持transformRequest interface ExtendedRequestInit extends RequestInit { transformRequest?: Array<(data: any, headers?: any) => any> @@ -108,24 +123,84 @@ class Request { } catch (error) { // 处理HTTP错误 const errorInfo = handleHttpError(error) - // 401 统一处理:清理本地状态并跳转登录 - try { - const status = (errorInfo as any)?.status || (error as any)?.status - if (status === 401) { - console.warn('[Auth] Token过期或无效,正在清理认证状态', { url, status }) - localStorage.removeItem('access_token') - localStorage.removeItem('refresh_token') - localStorage.removeItem('current_user') - // 避免死循环,仅在非登录页执行 - if (!location.pathname.startsWith('/login')) { - console.info('[Auth] 重定向到登录页') - location.href = '/login' + const status = (errorInfo as any)?.status || (error as any)?.status + + // 401 处理:先尝试刷新Token,失败后再跳转登录 + if (status === 401 && !url.includes('/auth/refresh') && !url.includes('/auth/login')) { + const refreshToken = localStorage.getItem('refresh_token') + + if (refreshToken) { + // 如果已经在刷新中,等待刷新完成后重试 + if (isRefreshing) { + return new Promise>((resolve, reject) => { + subscribeTokenRefresh((newToken: string) => { + // 使用新token重试原请求 + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${newToken}` + } + this.request(url, options, showLoading).then(resolve).catch(reject) + }) + }) + } + + isRefreshing = true + console.info('[Auth] Token过期,尝试刷新...') + + try { + // 调用刷新接口 + const refreshResponse = await fetch(`${API_CONFIG.baseURL}/api/v1/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refresh_token: refreshToken }) + }) + + if (refreshResponse.ok) { + const refreshData = await refreshResponse.json() + if (refreshData.code === 200 && refreshData.data?.token) { + const newAccessToken = refreshData.data.token.access_token + const newRefreshToken = refreshData.data.token.refresh_token + + // 保存新token + localStorage.setItem('access_token', newAccessToken) + if (newRefreshToken) { + localStorage.setItem('refresh_token', newRefreshToken) + } + + console.info('[Auth] Token刷新成功') + isRefreshing = false + onTokenRefreshed(newAccessToken) + + // 使用新token重试原请求 + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${newAccessToken}` + } + return this.request(url, options, showLoading) + } + } + + // 刷新失败,执行登出 + console.warn('[Auth] Token刷新失败,需要重新登录') + isRefreshing = false + } catch (refreshError) { + console.error('[Auth] Token刷新异常:', refreshError) + isRefreshing = false } } - } catch (authError) { - // 认证处理过程中的异常不应影响主流程,但需要记录 - console.error('[Auth] 处理401错误时发生异常:', authError) + + // 无refresh_token或刷新失败,清理状态并跳转登录 + console.warn('[Auth] Token过期或无效,正在清理认证状态', { url, status }) + localStorage.removeItem('access_token') + localStorage.removeItem('refresh_token') + localStorage.removeItem('current_user') + + if (!location.pathname.startsWith('/login')) { + console.info('[Auth] 重定向到登录页') + location.href = '/login' + } } + throw errorInfo } finally { if (showLoading) {