Files
leaudit-platform-frontend/docs/Token自动管理_测试说明.md
2025-12-05 00:09:32 +08:00

13 KiB
Raw Permalink Blame History

Token 自动管理 - 测试说明

修改摘要

目标

统一 Token 管理机制,完全依赖自动获取,移除所有手动传递 token 的代码。

架构说明

1. 客户端请求axios 拦截器自动处理)

浏览器环境中,所有使用 axiosInstance 的请求都会被 axios 拦截器自动添加 token

axios 拦截器 (app/api/axios-client.ts:66-86)

axiosInstance.interceptors.request.use((config) => {
  // 检查是否在白名单中
  if (isInAuthWhitelist(config.url)) {
    return config;
  }

  // ⚠️ 重要:只在浏览器环境中自动添加 token
  if (typeof window !== 'undefined') {
    const token = localStorage.getItem('access_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
  }

  return config;
});

受益的请求类型

  • 客户端组件中的 useEffectuseCallback、事件处理函数中的所有 API 请求
  • 所有 PostgREST 请求(postgrestGet, postgrestPost, postgrestPut, postgrestDelete
  • 所有使用 apiRequestdownloadFile 的请求

1.5. 服务端请求(需要显式传递 token

Remix Loader/Action(服务端) 中,typeof window === 'undefined'axios 拦截器不会自动添加 token。

解决方案:从 Cookie Session 获取 frontendJWT 并显式传递给 API 函数

示例 (app/routes/rules-files.tsx:62-74)

export async function loader({ request }: LoaderFunctionArgs) {
  // ✅ 从 Cookie Session 获取 frontendJWT
  const { getUserSession } = await import("~/api/login/auth.server");
  const { userInfo, frontendJWT } = await getUserSession(request);

  // ✅ 显式传递 token 给 API 函数
  const typesResponse = await getDocumentTypes({pageSize:500}, frontendJWT);

  return Response.json({ ... });
}

需要显式传递 token 的场景

  • 所有 Remix Loader 函数
  • 所有 Remix Action 函数
  • 任何在 Node.js 服务端运行的代码

2. 使用 fetch 的请求(手动处理)

不使用 axios 的请求需要手动从 localStorage 获取 token

示例 (app/api/files/files-upload.ts:appendContractAttachments)

// 设置请求头
const headers: HeadersInit = {
  'Accept': 'application/json'
};

// 从 localStorage 获取 token
if (typeof window !== 'undefined') {
  const token = localStorage.getItem('access_token');
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }
}

// 发送请求
const response = await fetch(uploadUrl, {
  method: 'POST',
  headers,
  body: formData
});

修改的文件

1. app/routes/rules-files.tsx

Loader 修改

  • getUserSession 获取 frontendJWTline 65
  • 传递给 getDocumentTypesline 74

客户端组件修改

  • 移除从 useLoaderData 中读取 frontendJWT
  • 移除所有客户端 API 调用中的 token 参数
  • 完全依赖 axios 拦截器自动添加 token

1.5. app/routes/documents.list.tsx

Loader 修改

  • getUserSession 获取 frontendJWTline 44
  • 传递给 getDocumentTypesline 52

客户端组件修改

  • 移除所有客户端 API 调用中的 token 参数
  • 完全依赖 axios 拦截器自动添加 token

2. app/api/evaluation_points/rules-files.ts

移除的内容

  • DocumentSearchParams.token 字段
  • getReviewFiles 函数的 token 参数处理
  • updateDocumentAuditStatus 函数的 token 参数
  • 所有 postgrestPost/postgrestPut 调用中的 token 参数

关键修改

// ❌ 修改前
export async function getReviewFiles(searchParams: DocumentSearchParams = {}, ...) {
  const { token } = searchParams;
  const filesResponse = await postgrestPost(..., listParams, token);
}

export async function updateDocumentAuditStatus(id, auditStatus, userId, token?) {
  const response = await postgrestPut(..., token);
}

// ✅ 修改后
export async function getReviewFiles(searchParams: DocumentSearchParams = {}, ...) {
  // token 参数已移除
  const filesResponse = await postgrestPost(..., listParams);  // axios 拦截器自动添加
}

export async function updateDocumentAuditStatus(id, auditStatus, userId) {
  const response = await postgrestPut(...);  // axios 拦截器自动添加
}

3. app/api/files/files-upload.ts

修改的内容

  • 移除 appendContractAttachmentsjwtToken 参数
  • 添加从 localStorage 获取 token 的逻辑(因为使用 fetch,不经过 axios 拦截器)

关键修改

// ❌ 修改前
export async function appendContractAttachments(..., jwtToken?: string) {
  if (jwtToken) {
    headers['Authorization'] = `Bearer ${jwtToken}`;
  }
}

// ✅ 修改后
export async function appendContractAttachments(...) {
  // 从 localStorage 获取 token
  if (typeof window !== 'undefined') {
    const token = localStorage.getItem('access_token');
    if (token) {
      headers['Authorization'] = `Bearer ${token}`;
    }
  }
}

测试步骤

1. 登录测试

目的:验证登录后 token 正确保存到 localStorage

步骤

  1. 打开浏览器开发者工具 → Application → Local Storage
  2. 访问登录页面并登录(单点登录或管理员登录)
  3. 检查 localStorage 中是否有 access_tokenuser_info

预期结果

access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
user_info: {"user_id":1,"username":"test",...}

2. 评查文件列表测试(PostgREST 请求)

目的:验证 axios 拦截器自动添加 token

步骤

  1. 打开浏览器开发者工具 → Network
  2. 访问 /rules-files 页面
  3. 查看 get_review_files_with_detailscount_review_files 请求
  4. 检查 Request Headers 中的 Authorization 字段

预期结果

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

验证代码

  • app/routes/rules-files.tsx:177 - fetchData 中调用 getReviewFiles
  • 不传递 token 参数,完全依赖 axios 拦截器

3. 完成评查按钮测试(PostgREST 请求)

目的:验证 PUT 请求也能自动添加 token

步骤

  1. 打开浏览器开发者工具 → Network
  2. 在评查文件列表中点击"完成评查"按钮
  3. 查看 documents?id=eq...&user_id=eq... 的 PATCH/PUT 请求
  4. 检查 Request Headers 中的 Authorization 字段

预期结果

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

验证代码

  • app/routes/rules-files.tsx:338 - handleReviewFileClick 中调用 updateDocumentAuditStatus
  • 不传递 token 参数,完全依赖 axios 拦截器

4. 附件追加测试(fetch 请求)

目的:验证使用 fetch 的请求也能正确添加 token

步骤

  1. 打开浏览器开发者工具 → Network
  2. 在评查文件列表中点击"追加附件"按钮,上传附件
  3. 查看 contracts/{id}/append_attachments 的 POST 请求
  4. 检查 Request Headers 中的 Authorization 字段

预期结果

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

验证代码

  • app/routes/rules-files.tsx:515 - handleAttachmentUpload 中调用 appendContractAttachments
  • 不传递 token 参数,函数内部从 localStorage 获取

5. 认证失败测试

目的:验证 token 过期或缺失时的行为

步骤

  1. 打开浏览器开发者工具 → Application → Local Storage
  2. 删除 access_token
  3. 刷新 /rules-files 页面
  4. 查看 Network 中的请求是否返回 401

预期结果

  • 请求返回 401 Unauthorized
  • 页面显示"获取文档列表失败: 用户身份验证失败"

6. 其他页面测试

目的:验证其他页面也能正常工作

页面列表

  • /documents - 文档列表(已修改)
  • /cross-checking - 交叉评查
  • /contract-template - 合同模板搜索
  • /chat-with-llm - AI 对话

步骤

  1. 访问每个页面
  2. 检查 Network 中的 API 请求是否都带有 Authorization header

Token 管理架构对比

修改前(混乱)

登录 → Cookie Session (frontendJWT)
       ↓
Loader → 传递 frontendJWT 给客户端
       ↓
Component → 手动传递 token 给 API 函数
           ↓
API 函数 → 接受 token 参数
           ↓
         → mergeAuthHeaders (从 token 参数或 localStorage)
           ↓
         → axios 拦截器 (再次从 localStorage)
           ↓
最终请求 → Authorization: Bearer xxx (重复添加)

问题

  • Token 来源混乱(Cookie Session vs localStorage
  • 重复添加 tokenmergeAuthHeaders + axios 拦截器)
  • 手动传递 token 容易遗漏
  • 代码冗余,维护困难

修改后(统一)

登录 → localStorage (access_token)
       ↓
Component → 不传递 token
           ↓
API 函数 → 不接受 token 参数
           ↓
         → axios 拦截器 (自动从 localStorage)
           ↓
最终请求 → Authorization: Bearer xxx

优点

  • 单一 token 来源(localStorage
  • 自动添加 tokenaxios 拦截器)
  • 无需手动传递
  • 代码简洁,易维护

潜在问题和解决方案

问题 1SSR 环境无法访问 localStorage

场景Remix loader/action 在服务端运行,无法访问 localStorage

解决方案

  • Loader/Action 中使用 Cookie SessiongetUserSession
  • 客户端组件使用 localStorage + axios 拦截器
  • 不需要在 loader 中传递 token 给客户端

问题 2Token 过期

场景Token 过期后请求返回 401

当前行为

  • axios 响应拦截器检测到 401
  • 清除 localStorage
  • 跳转到登录页

代码位置app/api/axios-client.ts:91-121

问题 3:使用 fetch 的请求

场景:不使用 axios 的请求无法被拦截器处理

解决方案

  • 在函数内部手动从 localStorage 获取 token
  • 示例:appendContractAttachments (已修改)

Token 管理流程图

┌─────────────────────────────────────────────────────────────────┐
│                       Token 管理架构                              │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────┐         ┌─────────────────────┐
│   客户端组件         │         │  服务端 Loader       │
│  (Browser)          │         │  (Node.js)          │
└──────────┬──────────┘         └──────────┬──────────┘
           │                               │
           │ API 调用                       │ API 调用
           │ (不传递 token)                 │ (显式传递 frontendJWT)
           ↓                               ↓
┌──────────────────────┐         ┌──────────────────────┐
│  axios 拦截器         │         │  API 函数            │
│  - 检查 window       │         │  - 接受 token 参数   │
│  - 从 localStorage   │         │  - 传递给            │
│    获取 token        │         │    postgrestGet/Post │
│  - 自动添加到 header │         └──────────┬───────────┘
└──────────┬───────────┘                    │
           │                                │
           │                                │ token 参数
           ↓                                ↓
┌─────────────────────────────────────────────────────────┐
│              axiosInstance (HTTP 请求)                   │
│  - Authorization: Bearer <token>                         │
└─────────────────────────────────────────────────────────┘

总结

通过这次重构,我们实现了:

  1. 统一 Token 来源

    • 客户端:从 localStorage 获取(axios 拦截器自动)
    • 服务端:从 Cookie Session 获取(显式传递)
  2. 自动 Token 注入

    • 客户端:axios 拦截器自动为所有请求添加 Authorization header
    • 服务端API 函数接受 optional token 参数
  3. 简化代码

    • 客户端组件无需手动传递 token
    • 服务端 loader 统一从 getUserSession 获取
  4. 易于维护Token 管理逻辑清晰分离

关键要点

  • ⚠️ Loader/Action 中必须显式传递 token(服务端无 localStorage
  • 客户端组件无需传递 tokenaxios 拦截器自动处理)
  • API 函数保留 optional token 参数(向后兼容)

下一步建议

  • 考虑将所有使用 fetch 的 API 改为使用 axios,统一请求方式
  • 统一 loader 中的错误处理模式