Skip to content

Latest commit

 

History

History
299 lines (236 loc) · 8.07 KB

File metadata and controls

299 lines (236 loc) · 8.07 KB

前端认证管理优化

问题描述

之前前端的认证管理存在以下问题:

  1. 401 错误处理不完整:只在响应拦截器的错误处理中处理 HTTP 401,但如果后端返回 {success: false, code: 401} 的业务错误(HTTP 200),不会触发跳转登录页
  2. 业务错误码未检查认证失败handleBusinessError 函数没有检查认证相关的业务错误码(401, 40101, 40102, 40103)
  3. Token 刷新可能失败但没有统一处理:某些接口可能返回 401 但没有正确跳转登录页
  4. 缺少 Token 自动刷新机制:Token 过期前没有自动刷新,导致用户操作时突然失效

优化方案

1. 统一认证错误处理

1.1 响应拦截器优化

文件: frontend/src/api/request.ts

优化内容

  • 在成功响应中检查业务错误码(401, 40101, 40102, 40103)
  • 优先处理认证错误,不依赖 skipErrorHandler 配置
  • 统一跳转登录页逻辑
// 响应拦截器 - 成功响应处理
instance.interceptors.response.use(
  (response: AxiosResponse) => {
    // 检查业务状态码
    const data = response.data as ApiResponse
    if (data && typeof data === 'object' && 'success' in data) {
      if (!data.success) {
        // 检查是否是认证错误(优先处理,不依赖 skipErrorHandler)
        const code = data.code
        if (code === 401 || code === 40101 || code === 40102 || code === 40103) {
          console.log('🔒 业务错误:认证失败 (HTTP 200),跳转登录页')
          authStore.clearAuthInfo()
          router.push('/login')
          ElMessage.error(data.message || '登录已过期,请重新登录')
          return Promise.reject(new Error(data.message || '认证失败'))
        }
        
        // 其他业务错误
        if (!config.skipErrorHandler) {
          handleBusinessError(data)
          return Promise.reject(new Error(data.message || '请求失败'))
        }
      }
    }

    return response.data
  },
  // ... 错误响应处理
)

1.2 业务错误处理优化

文件: frontend/src/api/request.ts

优化内容

  • handleBusinessError 函数中添加认证错误码处理
  • 统一认证错误的处理逻辑
const handleBusinessError = (data: ApiResponse) => {
  const { code, message } = data
  const authStore = useAuthStore()

  switch (code) {
    case 401:
    case 40101:  // 未授权
    case 40102:  // Token 无效
    case 40103:  // Token 过期
      console.log('🔒 业务错误:认证失败,跳转登录页')
      authStore.clearAuthInfo()
      router.push('/login')
      ElMessage.error(message || '登录已过期,请重新登录')
      break
    // ... 其他错误码处理
  }
}

2. 全局错误处理

文件: frontend/src/main.ts

优化内容

  • 在全局错误处理器中捕获未处理的认证错误
  • 确保所有认证错误都能跳转到登录页
app.config.errorHandler = (err, vm, info) => {
  console.error('全局错误:', err, info)
  
  // 检查是否是认证错误
  if (err && typeof err === 'object') {
    const error = err as any
    if (
      error.message?.includes('认证失败') ||
      error.message?.includes('登录已过期') ||
      error.message?.includes('Token') ||
      error.response?.status === 401 ||
      error.code === 401
    ) {
      console.log('🔒 全局错误处理:检测到认证错误,跳转登录页')
      const authStore = useAuthStore()
      authStore.clearAuthInfo()
      router.push('/login')
    }
  }
}

3. Token 自动刷新机制

文件: frontend/src/utils/auth.ts

新增功能

  • isTokenValid(): 检查 token 是否有效
  • parseToken(): 解析 token 获取 payload
  • getTokenRemainingTime(): 获取 token 剩余有效时间
  • isTokenExpiringSoon(): 检查 token 是否即将过期(默认 5 分钟)
  • autoRefreshToken(): 自动刷新即将过期的 token
  • setupTokenRefreshTimer(): 设置定时器,每分钟检查并刷新 token

使用方式

// 在应用初始化时启动定时器
import { setupTokenRefreshTimer } from '@/utils/auth'

// 用户登录成功后
if (authStore.isAuthenticated) {
  setupTokenRefreshTimer()
}

4. 认证工具函数

文件: frontend/src/utils/auth.ts

新增功能

  • isAuthError(): 检查是否是认证错误
  • handleAuthError(): 统一处理认证错误
  • Token 相关工具函数

使用示例

import { isAuthError, handleAuthError } from '@/utils/auth'

try {
  const response = await api.getData()
} catch (error) {
  if (isAuthError(error)) {
    handleAuthError(error)
  } else {
    // 处理其他错误
  }
}

认证错误码规范

HTTP 状态码

  • 401: 未授权(Unauthorized)

业务错误码

  • 401: 认证失败(通用)
  • 40101: 未授权(未登录)
  • 40102: Token 无效
  • 40103: Token 过期

认证流程

1. 登录流程

用户输入账号密码
    ↓
调用登录 API
    ↓
后端验证成功,返回 access_token 和 refresh_token
    ↓
前端保存 token 到 localStorage 和 Pinia store
    ↓
设置 axios 请求头 Authorization
    ↓
启动 token 自动刷新定时器
    ↓
跳转到目标页面

2. Token 刷新流程

定时器每分钟检查 token
    ↓
检查 token 是否即将过期(< 5 分钟)
    ↓
如果即将过期,调用刷新 API
    ↓
使用 refresh_token 获取新的 access_token
    ↓
更新 localStorage 和 Pinia store
    ↓
更新 axios 请求头

3. 认证失败处理流程

API 请求返回 401 或认证错误码
    ↓
响应拦截器捕获错误
    ↓
检查是否是 refresh 请求本身失败
    ↓
如果不是,尝试刷新 token
    ↓
如果刷新成功,重试原请求
    ↓
如果刷新失败,清除认证信息
    ↓
跳转到登录页
    ↓
显示错误提示

测试场景

1. Token 过期测试

场景: 用户长时间未操作,token 过期

预期行为:

  1. 用户操作时,API 返回 401
  2. 前端自动尝试刷新 token
  3. 如果刷新成功,继续原操作
  4. 如果刷新失败,跳转到登录页

2. Refresh Token 过期测试

场景: refresh_token 也过期了

预期行为:

  1. 用户操作时,API 返回 401
  2. 前端尝试刷新 token,但 refresh_token 也无效
  3. 清除认证信息
  4. 跳转到登录页
  5. 显示"登录已过期,请重新登录"

3. 业务错误码测试

场景: 后端返回 HTTP 200,但 {success: false, code: 401}

预期行为:

  1. 响应拦截器检测到业务错误码 401
  2. 清除认证信息
  3. 跳转到登录页
  4. 显示错误消息

4. Token 自动刷新测试

场景: Token 即将过期(剩余 < 5 分钟)

预期行为:

  1. 定时器检测到 token 即将过期
  2. 自动调用刷新 API
  3. 更新 token
  4. 用户无感知,继续操作

注意事项

  1. 避免无限循环: 如果 refresh 请求本身返回 401,不要再次尝试刷新
  2. 网络错误处理: 网络错误或服务器错误(5xx)不要清除认证信息
  3. 并发请求: 多个请求同时返回 401 时,只刷新一次 token
  4. 路由守卫: 确保路由守卫和 API 拦截器的认证逻辑一致
  5. Token 存储: 同时使用 localStorage 和 Pinia store,确保刷新页面后认证状态不丢失

相关文件

  • frontend/src/api/request.ts: API 请求拦截器和响应拦截器
  • frontend/src/stores/auth.ts: 认证状态管理
  • frontend/src/router/index.ts: 路由守卫
  • frontend/src/main.ts: 应用初始化和全局错误处理
  • frontend/src/utils/auth.ts: 认证工具函数
  • frontend/src/views/Auth/Login.vue: 登录页面

后续优化建议

  1. Token 刷新队列: 实现请求队列,避免并发刷新
  2. 离线模式: 支持离线缓存,网络恢复后自动同步
  3. 多标签页同步: 使用 BroadcastChannel 或 localStorage 事件同步多标签页的认证状态
  4. 安全增强: 实现 CSRF 保护、XSS 防护等安全措施
  5. 监控和日志: 集成前端监控服务,记录认证相关的错误和异常