SSE鉴权失效停止重连清理会话跳登录并降噪4xx错误日志

This commit is contained in:
yml2213
2026-08-23 11:01:41 +08:00
parent 8e49b7d047
commit 70b31f4874
4 changed files with 32 additions and 9 deletions
+2 -1
View File
@@ -56,7 +56,8 @@ export function sendRouteError(
logger(scope, fallbackMessage, {
statusCode: classification.statusCode,
errorCode: classification.errorCode,
error,
// 4xx 属预期失败(鉴权过期、参数错误等),带上完整错误对象只会刷十几行堆栈;仅 5xx 保留排查所需详情。
...(classification.statusCode >= 500 ? { error } : {}),
})
res.status(classification.statusCode).json(buildErrorPayload(error, fallbackMessage))
}
+19 -2
View File
@@ -21,16 +21,26 @@ type UseRealtimeEventsOptions = {
token: string
onEvent: (event: RealtimeEvent) => void
onConnected?: () => void
/** 鉴权失效(401/403)时触发:连接停止重连,调用方应清理会话并跳转登录页。 */
onAuthExpired?: () => void
}
const RECONNECT_DELAY_MS = 3_000
/** 通过 fetch 保持带 Bearer 鉴权的 SSE 连接,避免把登录 token 暴露到 URL。 */
export function useRealtimeEvents({ url, token, onEvent, onConnected }: UseRealtimeEventsOptions) {
export function useRealtimeEvents({
url,
token,
onEvent,
onConnected,
onAuthExpired,
}: UseRealtimeEventsOptions) {
const onEventRef = useRef(onEvent)
const onConnectedRef = useRef(onConnected)
const onAuthExpiredRef = useRef(onAuthExpired)
onEventRef.current = onEvent
onConnectedRef.current = onConnected
onAuthExpiredRef.current = onAuthExpired
useEffect(() => {
if (!token) return
@@ -50,6 +60,7 @@ export function useRealtimeEvents({ url, token, onEvent, onConnected }: UseRealt
Authorization: `Bearer ${token}`,
}
if (lastEventId > 0) headers['Last-Event-ID'] = String(lastEventId)
let authExpired = false
void fetch(url, {
method: 'GET',
headers,
@@ -57,6 +68,12 @@ export function useRealtimeEvents({ url, token, onEvent, onConnected }: UseRealt
signal: abortController.signal,
})
.then(async (response) => {
if (response.status === 401 || response.status === 403) {
// 会话已失效:重连只会无限 401,交给调用方清理会话跳登录。
authExpired = true
onAuthExpiredRef.current?.()
return
}
if (!response.ok || !response.body) {
throw new Error(`realtime_stream_${response.status}`)
}
@@ -73,7 +90,7 @@ export function useRealtimeEvents({ url, token, onEvent, onConnected }: UseRealt
})
.catch(() => undefined)
.finally(() => {
if (stopped || document.visibilityState !== 'visible') return
if (authExpired || stopped || document.visibilityState !== 'visible') return
reconnectTimer = window.setTimeout(connect, RECONNECT_DELAY_MS)
})
}
+6 -6
View File
@@ -87,6 +87,11 @@ export default function AdminLayout() {
url: '/api/v1/admin/realtime',
token: getAdminToken(),
onEvent: (event) => applyAdminRealtimeEvent(event, queryClient),
// 会话过期时清登录态回登录页,避免 SSE 带着失效 token 无限重连刷 401。
onAuthExpired: () => {
clearAdminSession()
void navigate('/admin/login', { replace: true, state: { from: location } })
},
})
const menuItems = useMemo<MenuProps['items']>(() => {
@@ -541,12 +546,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
}
type NotificationAlertKind =
| 'withdraw'
| 'recharge'
| 'acceptance'
| 'material'
| 'reminder'
| 'default'
'withdraw' | 'recharge' | 'acceptance' | 'material' | 'reminder' | 'default'
function playNotificationSound(kind: NotificationAlertKind) {
try {
@@ -45,6 +45,11 @@ export default function WorkerLayout() {
url: '/api/v1/worker/realtime',
token: getWorkerToken(),
onEvent: (event) => applyWorkerRealtimeEvent(event, queryClient),
// 会话过期时清登录态回登录页,避免 SSE 带着失效 token 无限重连刷 401。
onAuthExpired: () => {
clearWorkerSession()
void navigate('/', { replace: true })
},
})
async function submitLogout() {