From ac686c27e779af7bd5ed9511dcda86f774342ad0 Mon Sep 17 00:00:00 2001 From: yuliang_guo Date: Mon, 2 Feb 2026 17:35:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=AD=96=E7=95=A5=20-=20=E5=BB=B6=E9=95=BFtoken=E6=9C=89?= =?UTF-8?q?=E6=95=88=E6=9C=9F=E5=B9=B6=E6=94=AF=E6=8C=81=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 后端配置 (.env.ex): - ACCESS_TOKEN_EXPIRE_MINUTES: 30 -> 480 (8小时) 2. 前端 (request.ts): - 401错误时先尝试使用refresh_token刷新 - 刷新成功后自动重试原请求 - 支持并发请求时的token刷新队列 - 刷新失败才清除状态并跳转登录页 --- backend/.env.ex | 2 +- frontend/src/api/request.ts | 105 ++++++++++++++++++++++++++++++------ 2 files changed, 91 insertions(+), 16 deletions(-) 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) {