diff --git a/backend/src/common/auth/jwt.service.ts b/backend/src/common/auth/jwt.service.ts index 05900c55..67fa1a7b 100644 --- a/backend/src/common/auth/jwt.service.ts +++ b/backend/src/common/auth/jwt.service.ts @@ -51,9 +51,9 @@ export interface DecodedToken extends JWTPayload { } // Token 配置 -const ACCESS_TOKEN_EXPIRES_IN = '2h'; // Access Token 2小时过期 +const ACCESS_TOKEN_EXPIRES_IN = '24h'; // Access Token 24小时过期(长操作如审稿/文献检索需要充足时间) const REFRESH_TOKEN_EXPIRES_IN = '7d'; // Refresh Token 7天过期 -const ACCESS_TOKEN_EXPIRES_SECONDS = 2 * 60 * 60; // 7200秒 +const ACCESS_TOKEN_EXPIRES_SECONDS = 24 * 60 * 60; // 86400秒 /** * JWT Service 类 diff --git a/frontend-v2/src/common/api/axios.ts b/frontend-v2/src/common/api/axios.ts index acd20972..b75d68df 100644 --- a/frontend-v2/src/common/api/axios.ts +++ b/frontend-v2/src/common/api/axios.ts @@ -11,8 +11,8 @@ import { getAccessToken, getRefreshToken, refreshAccessToken, - clearTokens, } from '../../framework/auth/api'; +import { showSessionExpiredAndRedirect } from '../../framework/auth/sessionGuard'; const apiClient = axios.create({ timeout: 60000, @@ -68,11 +68,7 @@ apiClient.interceptors.response.use( if (!is401 || !hasRefreshToken || alreadyRetried || isKicked) { if (is401) { - clearTokens(); - if (isKicked) { - alert('您的账号已在其他设备登录,当前会话已失效,请重新登录'); - } - window.location.href = '/login'; + showSessionExpiredAndRedirect(isKicked ? 'kicked' : undefined); } return Promise.reject(error); } @@ -101,8 +97,7 @@ apiClient.interceptors.response.use( return apiClient(originalRequest); } catch (refreshError) { processPendingQueue(refreshError, null); - clearTokens(); - window.location.href = '/login'; + showSessionExpiredAndRedirect(); return Promise.reject(refreshError); } finally { isRefreshing = false; diff --git a/frontend-v2/src/framework/auth/index.ts b/frontend-v2/src/framework/auth/index.ts index 68dfa53a..845a59ad 100644 --- a/frontend-v2/src/framework/auth/index.ts +++ b/frontend-v2/src/framework/auth/index.ts @@ -5,6 +5,7 @@ export { AuthProvider, useAuth, AuthContext } from './AuthContext'; export * from './types'; export * from './api'; +export { handleFetchUnauthorized, showSessionExpiredAndRedirect } from './sessionGuard'; diff --git a/frontend-v2/src/framework/auth/sessionGuard.ts b/frontend-v2/src/framework/auth/sessionGuard.ts new file mode 100644 index 00000000..b0e39e97 --- /dev/null +++ b/frontend-v2/src/framework/auth/sessionGuard.ts @@ -0,0 +1,115 @@ +/** + * Session Guard - 全局会话过期处理 + * + * 提供统一的 401 处理逻辑,供 fetch 和 axios 客户端共用: + * 1. 尝试用 Refresh Token 自动续期 + * 2. 续期失败时弹出友好提示并跳转登录页 + */ + +import { getRefreshToken, refreshAccessToken, clearTokens } from './api'; + +let isRefreshing = false; +let pendingQueue: { + resolve: (token: string) => void; + reject: (err: unknown) => void; +}[] = []; + +function processPendingQueue(error: unknown, token: string | null) { + pendingQueue.forEach(({ resolve, reject }) => { + if (token) resolve(token); + else reject(error); + }); + pendingQueue = []; +} + +/** + * 显示友好的会话过期提示并跳转登录页 + */ +function showSessionExpiredAndRedirect(reason?: string) { + clearTokens(); + + const message = reason === 'kicked' + ? '您的账号已在其他设备登录,当前会话已失效' + : '登录已过期,请重新登录'; + + const overlay = document.createElement('div'); + overlay.id = 'session-expired-overlay'; + overlay.innerHTML = ` +
点击下方按钮返回登录页面
+ +