feat: 优化登录策略 - 延长token有效期并支持自动刷新
Some checks failed
continuous-integration/drone/push Build is failing
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:
@@ -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配置
|
||||
|
||||
@@ -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) {
|
||||
|
||||
// 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 })
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
localStorage.removeItem('current_user')
|
||||
// 避免死循环,仅在非登录页执行
|
||||
|
||||
if (!location.pathname.startsWith('/login')) {
|
||||
console.info('[Auth] 重定向到登录页')
|
||||
location.href = '/login'
|
||||
}
|
||||
}
|
||||
} catch (authError) {
|
||||
// 认证处理过程中的异常不应影响主流程,但需要记录
|
||||
console.error('[Auth] 处理401错误时发生异常:', authError)
|
||||
}
|
||||
|
||||
throw errorInfo
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
|
||||
Reference in New Issue
Block a user