fix(auth): 修复登录续期令牌不同步与并发过期误判
This commit is contained in:
@@ -55,12 +55,14 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
...((options.headers as Record<string, string>) || {}),
|
||||
}
|
||||
// 记录本次请求实际使用的令牌,避免旧请求响应覆盖新会话。
|
||||
const requestToken = token
|
||||
// FormData 时不要手动设置 Content-Type,由浏览器带 boundary
|
||||
const isForm = typeof FormData !== 'undefined' && options.body instanceof FormData
|
||||
if (!isForm && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
if (requestToken) headers['Authorization'] = `Bearer ${requestToken}`
|
||||
|
||||
const res = await fetch(`${BASE}${url}`, { ...options, headers })
|
||||
const nextPermissionVersion = res.headers.get('X-Permission-Version') || ''
|
||||
@@ -73,17 +75,16 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
}
|
||||
// 服务端滑动续期:静默替换本地令牌
|
||||
const renewed = res.headers.get('X-Renewed-Token')
|
||||
if (renewed && renewed !== token) {
|
||||
if (renewed && requestToken === token && renewed !== token) {
|
||||
setToken(renewed)
|
||||
tokenRenewedListener?.(renewed)
|
||||
}
|
||||
const json = await res.json()
|
||||
|
||||
if (json.code === 401) {
|
||||
// 登录/续期请求本身的 401 交给调用方提示,不触发全局过期
|
||||
if (!url.startsWith('/login') && !url.startsWith('/auth/refresh')) {
|
||||
notifySessionExpired()
|
||||
}
|
||||
// 只处理当前会话令牌的 401;登录失败和已经被新令牌替换的旧请求不触发全局登出。
|
||||
if ((res.status === 401 || json.code === 401) && url !== '/login' &&
|
||||
requestToken !== '' && requestToken === token) {
|
||||
notifySessionExpired()
|
||||
}
|
||||
|
||||
if (json.code !== 0) {
|
||||
|
||||
+18
-12
@@ -1,6 +1,6 @@
|
||||
import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react'
|
||||
import {
|
||||
PERMISSIONS_CHANGED_EVENT, SESSION_EXPIRED_EVENT, onTokenRenewed, setToken, resetSessionExpiredFlag,
|
||||
PERMISSIONS_CHANGED_EVENT, SESSION_EXPIRED_EVENT, getToken, onTokenRenewed, setToken, resetSessionExpiredFlag,
|
||||
} from '@/services/request'
|
||||
import {
|
||||
getMe,
|
||||
@@ -141,28 +141,34 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 服务端滑动续期(X-Renewed-Token):同步更新内存与 localStorage 中的令牌
|
||||
useEffect(() => {
|
||||
return onTokenRenewed(nextToken => {
|
||||
setUser(prev => {
|
||||
if (!prev) return prev
|
||||
const updated = { ...prev, token: nextToken }
|
||||
persistUser(updated)
|
||||
return updated
|
||||
})
|
||||
// 所有续期入口共用:同步请求模块、React 状态和 localStorage 中的令牌
|
||||
const applyRenewedToken = useCallback((nextToken: string) => {
|
||||
if (!nextToken) return
|
||||
setToken(nextToken)
|
||||
setUser(prev => {
|
||||
if (!prev || prev.token === nextToken) return prev
|
||||
const updated = { ...prev, token: nextToken }
|
||||
persistUser(updated)
|
||||
return updated
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 服务端滑动续期(X-Renewed-Token)
|
||||
useEffect(() => onTokenRenewed(applyRenewedToken), [applyRenewedToken])
|
||||
|
||||
// 定时续期:长挂机场景下每 10 分钟主动换新令牌,避免 WS 重连/请求时已过期
|
||||
useEffect(() => {
|
||||
if (!user?.token) return
|
||||
const sessionToken = user.token
|
||||
const timer = window.setInterval(() => {
|
||||
void refreshSession().then(res => {
|
||||
if (res.data?.token) setToken(res.data.token)
|
||||
// 请求期间已退出、重新登录或由其它响应完成续期时,不再应用旧响应。
|
||||
if (getToken() !== sessionToken) return
|
||||
applyRenewedToken(res.data?.token || '')
|
||||
}).catch(() => { /* 401 已由 request 层统一处理;网络错误等待下一轮 */ })
|
||||
}, 10 * 60 * 1000)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [user?.token])
|
||||
}, [applyRenewedToken, user?.token])
|
||||
|
||||
// 会话过期(所有 401 的统一出口):清理登录态并回到登录页
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user