diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index beb03d7..f3f35d5 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -13,6 +13,7 @@ import { DownloadPanel } from '../components/DownloadPanel';
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
import { SiteProvider } from '../components/SiteProvider';
import { ThemeProvider } from '../components/ThemeProvider';
+import { TokenRefreshManager } from '../components/TokenRefreshManager';
import TopProgressBar from '../components/TopProgressBar';
import ChatFloatingWindow from '../components/watch-room/ChatFloatingWindow';
import { WatchRoomProvider } from '../components/WatchRoomProvider';
@@ -222,6 +223,7 @@ export default async function RootLayout({
disableTransitionOnChange
>
+
diff --git a/src/components/TokenRefreshManager.tsx b/src/components/TokenRefreshManager.tsx
new file mode 100644
index 0000000..ddad9ab
--- /dev/null
+++ b/src/components/TokenRefreshManager.tsx
@@ -0,0 +1,141 @@
+'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;
+}
diff --git a/src/middleware.ts b/src/middleware.ts
index 8862515..2ad737d 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -3,7 +3,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
-import { refreshAccessToken, shouldRenewToken } from '@/lib/middleware-auth';
import { TOKEN_CONFIG } from '@/lib/refresh-token';
export async function middleware(request: NextRequest) {
@@ -54,37 +53,9 @@ export async function middleware(request: NextRequest) {
const now = Date.now();
const age = now - authInfo.timestamp;
- // Access Token 已过期,尝试使用 Refresh Token 刷新
+ // Access Token 已过期,前端负责刷新
if (age > ACCESS_TOKEN_AGE) {
- if (authInfo.refreshToken && authInfo.tokenId && authInfo.refreshExpires) {
- // 检查 Refresh Token 是否过期
- if (now < authInfo.refreshExpires) {
- // 尝试刷新 Access Token
- const newAuthData = await refreshAccessToken(
- authInfo.username,
- authInfo.role,
- authInfo.tokenId,
- authInfo.refreshToken,
- authInfo.refreshExpires
- );
-
- if (newAuthData) {
- // 刷新成功,设置新 Cookie
- const response = NextResponse.next();
- const expires = new Date(authInfo.refreshExpires);
- response.cookies.set('auth', newAuthData, {
- path: '/',
- expires,
- sameSite: 'lax',
- httpOnly: false,
- secure: false,
- });
- return response;
- }
- }
- }
-
- // Refresh Token 也过期或刷新失败,需要重新登录
+ console.log(`Access token expired for ${authInfo.username}, redirecting to login`);
return handleAuthFailure(request, pathname);
}
@@ -101,34 +72,8 @@ export async function middleware(request: NextRequest) {
return handleAuthFailure(request, pathname);
}
- // 签名验证通过,检查是否需要续期
- if (shouldRenewToken(authInfo.timestamp)) {
- // 快过期了,自动续期
- if (authInfo.refreshToken && authInfo.tokenId && authInfo.refreshExpires) {
- const newAuthData = await refreshAccessToken(
- authInfo.username,
- authInfo.role,
- authInfo.tokenId,
- authInfo.refreshToken,
- authInfo.refreshExpires
- );
-
- if (newAuthData) {
- const response = NextResponse.next();
- const expires = new Date(authInfo.refreshExpires);
- response.cookies.set('auth', newAuthData, {
- path: '/',
- expires,
- sameSite: 'lax',
- httpOnly: false,
- secure: false,
- });
- return response;
- }
- }
- }
-
- // 正常通过
+ // 签名验证通过
+ // 注意:Token 续期由前端负责,Middleware 不再自动刷新
return NextResponse.next();
}