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
|
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配置
|
||||||
|
|||||||
@@ -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 统一处理:清理本地状态并跳转登录
|
const status = (errorInfo as any)?.status || (error as any)?.status
|
||||||
try {
|
|
||||||
const status = (errorInfo as any)?.status || (error as any)?.status
|
// 401 处理:先尝试刷新Token,失败后再跳转登录
|
||||||
if (status === 401) {
|
if (status === 401 && !url.includes('/auth/refresh') && !url.includes('/auth/login')) {
|
||||||
console.warn('[Auth] Token过期或无效,正在清理认证状态', { url, status })
|
const refreshToken = localStorage.getItem('refresh_token')
|
||||||
localStorage.removeItem('access_token')
|
|
||||||
localStorage.removeItem('refresh_token')
|
if (refreshToken) {
|
||||||
localStorage.removeItem('current_user')
|
// 如果已经在刷新中,等待刷新完成后重试
|
||||||
// 避免死循环,仅在非登录页执行
|
if (isRefreshing) {
|
||||||
if (!location.pathname.startsWith('/login')) {
|
return new Promise<ApiResponse<T>>((resolve, reject) => {
|
||||||
console.info('[Auth] 重定向到登录页')
|
subscribeTokenRefresh((newToken: string) => {
|
||||||
location.href = '/login'
|
// 使用新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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (authError) {
|
|
||||||
// 认证处理过程中的异常不应影响主流程,但需要记录
|
// 无refresh_token或刷新失败,清理状态并跳转登录
|
||||||
console.error('[Auth] 处理401错误时发生异常:', authError)
|
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
|
throw errorInfo
|
||||||
} finally {
|
} finally {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
|
|||||||
Reference in New Issue
Block a user