Files
MoonTVPlus/src/components/TokenRefreshManager.tsx
T
2026-01-27 09:08:44 +08:00

142 lines
4.3 KiB
TypeScript

'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<boolean> | null = null;
// Token 刷新函数
const refreshToken = async (): Promise<boolean> => {
// 如果正在刷新,返回现有的 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<Response> => {
// 跳过刷新 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;
}