diff --git a/src/app/api/auth/refresh/route.ts b/src/app/api/auth/refresh/route.ts index 667f6d5..d0f2dfe 100644 --- a/src/app/api/auth/refresh/route.ts +++ b/src/app/api/auth/refresh/route.ts @@ -72,24 +72,8 @@ export async function POST(request: NextRequest) { } const now = Date.now(); - const accessTokenAge = now - authInfo.timestamp; - const remainingAccessTime = TOKEN_CONFIG.ACCESS_TOKEN_AGE - accessTokenAge; - const refreshWindow = 15 * 60 * 1000; - - if (remainingAccessTime <= 0) { - return NextResponse.json( - { error: 'Access token expired' }, - { status: 401 } - ); - } - - if (remainingAccessTime > refreshWindow) { - return NextResponse.json( - { error: 'Refresh not allowed' }, - { status: 400 } - ); - } + // 只检查 Refresh Token 是否过期 if (now >= authInfo.refreshExpires) { return NextResponse.json( { error: 'Refresh token expired' }, @@ -97,6 +81,8 @@ export async function POST(request: NextRequest) { ); } + // 只要 Refresh Token 有效,就允许刷新(即使 Access Token 已过期) + const newAuthData = await refreshAccessToken( authInfo.username, authInfo.role, diff --git a/src/components/TokenRefreshManager.tsx b/src/components/TokenRefreshManager.tsx index 28fe0f7..375171e 100644 --- a/src/components/TokenRefreshManager.tsx +++ b/src/components/TokenRefreshManager.tsx @@ -89,9 +89,9 @@ export function TokenRefreshManager() { const age = now - authInfo.timestamp; const remaining = ACCESS_TOKEN_AGE - age; - // 剩余时间 < 10 分钟时需要刷新 + // 剩余时间 < 10 分钟时需要刷新(包括已过期的情况) const REFRESH_THRESHOLD = 10 * 60 * 1000; // 10 分钟 - return remaining < REFRESH_THRESHOLD && remaining > 0; + return remaining < REFRESH_THRESHOLD; }; // 保存原始 fetch @@ -132,7 +132,7 @@ export function TokenRefreshManager() { // 刷新成功,重试原请求(仅此一次) response = await originalFetch(input, init); - // 如果重试后仍然是 401,说明有其他问题,不再重试 + // 如果重试后仍然是 401,说明有问题,跳转登录 if (response.status === 401) { console.error('[Token] Still 401 after refresh, redirecting to login'); window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}`; diff --git a/src/lib/db.client.ts b/src/lib/db.client.ts index d6cdc13..4d67aa2 100644 --- a/src/lib/db.client.ts +++ b/src/lib/db.client.ts @@ -521,9 +521,25 @@ async function fetchWithAuth( url: string, options?: RequestInit ): Promise { - const res = await fetch(url, options); - if (!res.ok) { - // 如果是 401 未授权,跳转到登录页面 + let res = await fetch(url, options); + + // 如果是 401 且是 token 过期,尝试刷新并重试 + if (res.status === 401) { + const text = await res.clone().text(); + if (text === 'Access token expired') { + // 尝试刷新 token + const refreshRes = await fetch('/api/auth/refresh', { + method: 'POST', + credentials: 'include', + }); + + if (refreshRes.ok) { + // 刷新成功,重试原请求 + res = await fetch(url, options); + } + } + + // 如果刷新后仍然是 401,或者是其他 401 错误,跳转登录 if (res.status === 401) { // 调用 logout 接口 try { @@ -540,8 +556,12 @@ async function fetchWithAuth( window.location.href = loginUrl.toString(); throw new Error('用户未授权,已跳转到登录页面'); } + } + + if (!res.ok) { throw new Error(`请求 ${url} 失败: ${res.status}`); } + return res; } diff --git a/src/middleware.ts b/src/middleware.ts index 2ad737d..ff629bb 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -48,17 +48,29 @@ export async function middleware(request: NextRequest) { return handleAuthFailure(request, pathname); } - // 验证 Access Token 时间戳 + // 验证 Token 时间戳 const ACCESS_TOKEN_AGE = TOKEN_CONFIG.ACCESS_TOKEN_AGE; const now = Date.now(); const age = now - authInfo.timestamp; - // Access Token 已过期,前端负责刷新 - if (age > ACCESS_TOKEN_AGE) { - console.log(`Access token expired for ${authInfo.username}, redirecting to login`); + // 先检查 Refresh Token 是否过期 + if (now >= authInfo.refreshExpires) { + console.log(`Refresh token expired for ${authInfo.username}, redirecting to login`); return handleAuthFailure(request, pathname); } + // Access Token 已过期 + if (age > ACCESS_TOKEN_AGE) { + console.log(`Access token expired for ${authInfo.username}`); + // 对于 API 请求,返回 401,让前端拦截器刷新并重试 + if (pathname.startsWith('/api')) { + return new NextResponse('Access token expired', { status: 401 }); + } + // 对于页面请求,允许通过,让前端 TokenRefreshManager 在页面加载后刷新 + // 不能返回 401 或重定向,否则页面无法加载,前端代码无法运行 + console.log(`Allowing page request to pass, frontend will refresh token`); + } + // Access Token 未过期,验证签名 const isValidSignature = await verifySignature( authInfo.username,