'use client'; import { useEffect } from 'react'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; /** * Token 自动刷新管理器 * * 功能: * 1. 拦截所有 fetch 请求 * 2. 检测到 401 错误时自动刷新 Token 并重试 * 3. 在请求前检查 Token 是否即将过期,主动刷新 * * 策略: * - 响应拦截:401 错误 → 刷新 Token → 重试请求 * - 请求拦截:剩余时间 < 10 分钟 → 主动刷新 */ export function TokenRefreshManager() { useEffect(() => { // localStorage 模式不需要刷新 const storageType = (window as any).RUNTIME_CONFIG?.STORAGE_TYPE || 'localstorage'; if (storageType === 'localstorage') { return; } // 刷新状态管理 let isRefreshing = false; let refreshPromise: Promise | null = null; // Token 刷新函数 const refreshToken = async (): Promise => { // 如果正在刷新,返回现有的 Promise if (isRefreshing && refreshPromise) { return refreshPromise; } isRefreshing = true; refreshPromise = (async () => { try { // 使用原始 fetch 避免递归 const response = await window.fetch('/api/auth/refresh', { method: 'POST', credentials: 'include', }); if (response.ok) { console.log('[Token] Refreshed successfully'); return true; } else { console.error('[Token] Refresh failed:', response.status); // 刷新失败,跳转登录 if (response.status === 401 || response.status === 403) { window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}`; } return false; } } catch (error) { console.error('[Token] Refresh error:', error); return false; } finally { isRefreshing = false; refreshPromise = null; } })(); return refreshPromise; }; // 检查 Token 是否需要刷新 const shouldRefreshToken = (): boolean => { const authInfo = getAuthInfoFromBrowserCookie(); if (!authInfo || !authInfo.timestamp || !authInfo.refreshExpires) { return false; } const now = Date.now(); // Refresh Token 已过期 if (now >= authInfo.refreshExpires) { console.log('[Token] Refresh token expired, redirecting to login'); window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}`; return false; } // 计算 Access Token 剩余时间 const ACCESS_TOKEN_AGE = 4 * 60 * 60 * 1000; // 4 小时 const age = now - authInfo.timestamp; const remaining = ACCESS_TOKEN_AGE - age; // 剩余时间 < 10 分钟时需要刷新 const REFRESH_THRESHOLD = 10 * 60 * 1000; // 10 分钟 return remaining < REFRESH_THRESHOLD && remaining > 0; }; // 保存原始 fetch const originalFetch = window.fetch; // 拦截 fetch window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise => { // 跳过刷新 API 本身 const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; if (url.includes('/api/auth/refresh')) { return originalFetch(input, init); } // 请求前检查:Token 即将过期时主动刷新 if (shouldRefreshToken() && !isRefreshing) { console.log('[Token] Expiring soon, refreshing proactively...'); await refreshToken(); } // 发送请求 let response = await originalFetch(input, init); // 响应拦截:401 错误时刷新 Token 并重试 if (response.status === 401 && !isRefreshing) { console.log('[Token] Received 401, attempting refresh and retry...'); const refreshed = await refreshToken(); if (refreshed) { // 刷新成功,重试原请求 response = await originalFetch(input, init); } } return response; }; // 清理:恢复原始 fetch return () => { window.fetch = originalFetch; }; }, []); // 这是一个纯逻辑组件,不渲染任何内容 return null; }