# Changelog: 认证失效处理优化 **日期**: 2026-02-09 **类型**: Bug Fix / Enhancement **影响范围**: 前端认证机制 --- ## 问题背景 ### 现象 用户在项目页面中,如果遇到以下情况: 1. **项目被删除**(404)→ token 被错误清除 → 跳转登录页(❌ 错误) 2. **权限不足**(403)→ token 被错误清除 → 跳转登录页(❌ 错误) 3. **Token 失效**(401)→ 重复调用 clearAuth → 多次跳转登录页(❌ 错误) ### 根本原因 - **误判认证错误**:403(权限不足)和 404(资源不存在)不应清除 token - **重复清除**:并发 401 请求多次触发 clearAuth - **跳转逻辑混乱**:`client.ts` 和 `AuthContext` 都有跳转逻辑 --- ## 解决方案 ### 核心改进 #### 1. **严格区分 HTTP 状态码** | 状态码 | 含义 | 是否清除 Token | 跳转页面 | |--------|------|----------------|----------| | **401** | 未认证/Token 失效 | ✅ 是 | `/login` | | **403** | 权限不足(已登录) | ❌ 否 | 无(显示错误提示) | | **404** | 资源不存在 | ❌ 否 | `/workspace` | **理由**: - **401**:Token 过期或无效,必须重新登录 - **403**:用户已登录但无权访问该资源,应保持登录状态 - **404**:资源被删除,跳转到 workspace 而非登录页 #### 2. **使用 sessionStorage 持久化标记** ```typescript // AuthContext.tsx const clearAuth = useCallback((reason?: string) => { const isClearing = sessionStorage.getItem('auth_clearing'); if (isClearing === 'true') { console.log('⚠️ 认证已失效,跳过重复清除'); return; } sessionStorage.setItem('auth_clearing', 'true'); // 会话级保护 // ... 清除 token + 跳转 }, [setUser, queryClient]); ``` **优点**: - 会话期间全局生效,防止任何重复调用 - 刷新页面后自动清除(不影响正常使用) - 无需定时器,避免竞态条件 #### 3. **请求拦截器增强** ```typescript // client.ts apiClient.interceptors.request.use((config) => { const isClearing = sessionStorage.getItem('auth_clearing'); if (isClearing === 'true') { console.log('🚫 认证已失效,拒绝请求:', config.url); return Promise.reject(new Error('认证失效,请重新登录')); } // ... 添加 token }); ``` **效果**: - 一旦检测到 401,**立即阻止所有后续请求** - 避免并发请求重复触发 clearAuth - 减少无效请求,降低服务器负载 #### 4. **响应拦截器分层处理** ```typescript // 401: 清除 token + 跳转登录 if (errorData.code === 401) { clearAuth(`API 错误 401: ${message}`); } // 403: 保持登录状态 if (errorData.code === 403) { console.warn('⚠️ 权限不足 (403)'); // 不清除 token } // 404: 跳转 workspace if (errorData.code === 404) { console.warn('⚠️ 资源不存在 (404)'); window.location.href = '/workspace'; } ``` #### 5. **统一跳转逻辑** - ✅ **保留**:`AuthContext.clearAuth` 中的 401 跳转逻辑 - ✅ **新增**:404 跳转到 `/workspace` - ❌ **移除**:多处重复跳转逻辑 #### 6. **登录时清除标记** #### 5. **登录时清除标记** ```typescript // AuthContext.tsx const login = useCallback(async (token: string, userData: User) => { sessionStorage.removeItem('auth_clearing'); // 清除失效标记 localStorage.setItem('token', token); // ... }, [setUser]); ``` **作用**: - 登录成功后恢复正常请求 - 双重保险:确保标记不会残留 #### 6. **登录页清除标记** ```typescript // LoginPage.tsx useEffect(() => { // 清除认证失效标记(到达登录页即可重新登录) sessionStorage.removeItem('auth_clearing'); // ... 检测已登录状态 }, [navigate, from]); ``` **作用**: - 到达登录页时立即清除标记 - 允许用户重新登录(不被请求拦截器拒绝) - 确保登录流程正常进行 ## 变更文件 ### 1. `client/src/contexts/AuthContext.tsx` - ✅ 使用 `sessionStorage.getItem('auth_clearing')` 替代 `clearingRef` - ✅ 移除 `useRef` 导入和 100ms 定时器 - ✅ 在 `login` 中清除 `auth_clearing` 标记 - ✅ 在 `clearAuth` 中统一处理跳转逻辑 ### 2. `client/src/services/api/client.ts` - ✅ 请求拦截器:检测 `auth_clearing` 标记,拒绝新请求 - ✅ 响应拦截器:分层处理 401/403/404 - **401**:调用 `clearAuth()`(清除 token + 跳转登录) - **403**:仅 console.warn(不清除 token) - **404**:跳转 `/workspace`(不清除 token) - ✅ 兼容旧格式(`error.response.status`) ### 3. `client/src/lib/error-utils.ts` - ✅ 更新 `isAuthError()`:仅 401 返回 true(不包含 403) - ✅ 增加注释说明:403 是权限不足但仍是登录状态 ### 4. `client/src/pages/LoginPage.tsx` - ✅ 在 `useEffect` 中清除 `auth_clearing` 标记(到达登录页时) - ✅ 确保登录请求不被拦截器拒绝 ### 5. `client/src/lib/auth-manager.ts` - ✅ 移除降级逻辑(`localStorage.removeItem('token')`) - ✅ 未注册时不执行任何操作(避免状态不一致) --- ## 测试场景 ### ✅ 场景 1:Token 失效(401) **操作**:调用一个返回 401 的 API **预期**: - 执行 `clearAuth` 一次 - 清除 token + 用户状态 - 跳转到登录页 - 设置 `auth_clearing` 标记 ### ✅ 场景 2:并发 5 个 401 请求 **操作**:同时发起 5 个返回 401 的 API 请求 **预期**: - 第 1 个请求触发 `clearAuth` - 其余 4 个请求检测到标记,直接被拦截(不再触发 clearAuth) - 只跳转一次登录页 - Console 显示 4 次 `🚫 认证已失效,拒绝请求` ### ✅ 场景 3:权限不足(403) **操作**:调用一个返回 403 的 API **预期**: - **不清除 token**(用户仍是登录状态) - **不跳转登录页** - Console 显示 `⚠️ 权限不足 (403)` - 用户可继续访问其他有权限的资源 ### ✅ 场景 4:资源不存在(404) **操作**:访问一个已被删除的项目 **预期**: - **不清除 token** - **跳转到 `/workspace`**(而非登录页) - Console 显示 `⚠️ 资源不存在 (404)` - 用户可在 workspace 中选择其他项目 ### ✅ 场景 5:401 后尝试发起新请求 **操作**: 1. 触发 401 错误 2. 在跳转前,手动调用其他 API **预期**: - 新请求被拦截,返回 `认证失效,请重新登录` 错误 - 不会发送到服务器 ### ✅ 场景 6:登录页加载时清除标记 **操作**: 1. 触发 401(设置 `auth_clearing = true`) 2. 跳转到登录页 **预期**: - 登录页加载时自动清除 `auth_clearing` 标记 - Console 显示 `✅ LoginPage: 清除 auth_clearing 标记` - 登录请求不被拦截器拒绝 ### ✅ 场景 7:登录成功后恢复 **操作**: 1. 触发 401(设置 `auth_clearing = true`) 2. 跳转到登录页 3. 输入手机号 + 验证码登录 **预期**: - 登录成功后清除 `auth_clearing` 标记 - 后续 API 请求正常发送 - 请求拦截器不再拒绝请求 --- ## 影响评估 ### ✅ 正面影响 1. **正确区分错误类型**:403/404 不再错误清除 token 2. **优化用户体验**:404 跳转 workspace 而非登录页 3. **彻底解决重复清除**:并发 401 只触发一次 clearAuth 4. **减少无效请求**:阻止认证失效后的所有新请求 ### ⚠️ 潜在风险 1. **sessionStorage 依赖**:部分浏览器隐私模式可能禁用 sessionStorage - **缓解措施**:保留 `isClearing` 状态作为备用标记 2. **标记未清除**:极端情况下(页面崩溃),标记可能残留 - **缓解措施**:刷新页面后自动清除(sessionStorage 特性) --- ## 后续优化建议 1. **增强用户体验** - 403 错误时显示友好提示(如 Toast:"您没有权限访问此资源") - 404 错误时显示友好提示(如 Toast:"资源不存在,已返回工作区") - 提供"联系管理员"按钮(403 场景) 2. **监控和日志** - 上报 401/403/404 频率(识别异常行为) - 记录 `auth_clearing` 触发时间和原因 3. **测试覆盖** - 添加 E2E 测试:模拟并发 401 请求 - 添加单元测试:验证 `clearAuth` 防重入逻辑 - 添加集成测试:验证 403/404 不清除 token --- ## 参考 - **相关文件**: - `client/src/contexts/AuthContext.tsx` - `client/src/services/api/client.ts` - `client/src/lib/auth-manager.ts` - `client/src/lib/error-utils.ts` - **关联文档**: - RFC 135: 统一响应格式 - ADR: 认证架构设计 --- **作者**: Claude **审阅**: 待审阅 **状态**: ✅ 已实施