feat: 优化登录策略 - 延长token有效期并支持自动刷新
Some checks failed
continuous-integration/drone/push Build is failing

1. 后端配置 (.env.ex):
   - ACCESS_TOKEN_EXPIRE_MINUTES: 30 -> 480 (8小时)

2. 前端 (request.ts):
   - 401错误时先尝试使用refresh_token刷新
   - 刷新成功后自动重试原请求
   - 支持并发请求时的token刷新队列
   - 刷新失败才清除状态并跳转登录页
This commit is contained in:
yuliang_guo
2026-02-02 17:35:29 +08:00
parent 58f746cf46
commit ac686c27e7
2 changed files with 91 additions and 16 deletions

View File

@@ -22,7 +22,7 @@ REDIS_DB=0
# 安全配置 # 安全配置
SECRET_KEY=ex_8f7a9c3e1b4d6f2a5c8e7b9d1f3a6c4e8b2d5f7a9c1e3b6d8f2a4c7e9b1d3f5a SECRET_KEY=ex_8f7a9c3e1b4d6f2a5c8e7b9d1f3a6c4e8b2d5f7a9c1e3b6d8f2a4c7e9b1d3f5a
ALGORITHM=HS256 ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30 ACCESS_TOKEN_EXPIRE_MINUTES=480
REFRESH_TOKEN_EXPIRE_DAYS=7 REFRESH_TOKEN_EXPIRE_DAYS=7
# CORS配置 # CORS配置

View File

@@ -10,6 +10,21 @@ import { loadingManager } from '@/utils/loadingManager'
// 模拟延迟,使体验更真实 // 模拟延迟,使体验更真实
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) 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 // 扩展RequestInit接口以支持transformRequest
interface ExtendedRequestInit extends RequestInit { interface ExtendedRequestInit extends RequestInit {
transformRequest?: Array<(data: any, headers?: any) => any> transformRequest?: Array<(data: any, headers?: any) => any>
@@ -108,24 +123,84 @@ class Request {
} catch (error) { } catch (error) {
// 处理HTTP错误 // 处理HTTP错误
const errorInfo = handleHttpError(error) const errorInfo = handleHttpError(error)
// 401 统一处理:清理本地状态并跳转登录
try {
const status = (errorInfo as any)?.status || (error as any)?.status const status = (errorInfo as any)?.status || (error as any)?.status
if (status === 401) {
// 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<ApiResponse<T>>((resolve, reject) => {
subscribeTokenRefresh((newToken: string) => {
// 使用新token重试原请求
options.headers = {
...options.headers,
'Authorization': `Bearer ${newToken}`
}
this.request<T>(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<T>(url, options, showLoading)
}
}
// 刷新失败,执行登出
console.warn('[Auth] Token刷新失败需要重新登录')
isRefreshing = false
} catch (refreshError) {
console.error('[Auth] Token刷新异常:', refreshError)
isRefreshing = false
}
}
// 无refresh_token或刷新失败清理状态并跳转登录
console.warn('[Auth] Token过期或无效正在清理认证状态', { url, status }) console.warn('[Auth] Token过期或无效正在清理认证状态', { url, status })
localStorage.removeItem('access_token') localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token') localStorage.removeItem('refresh_token')
localStorage.removeItem('current_user') localStorage.removeItem('current_user')
// 避免死循环,仅在非登录页执行
if (!location.pathname.startsWith('/login')) { if (!location.pathname.startsWith('/login')) {
console.info('[Auth] 重定向到登录页') console.info('[Auth] 重定向到登录页')
location.href = '/login' location.href = '/login'
} }
} }
} catch (authError) {
// 认证处理过程中的异常不应影响主流程,但需要记录
console.error('[Auth] 处理401错误时发生异常:', authError)
}
throw errorInfo throw errorInfo
} finally { } finally {
if (showLoading) { if (showLoading) {