This commit is contained in:
2025-12-05 00:09:32 +08:00
parent bb3d22eabf
commit 3d1dbb3f97
214 changed files with 113060 additions and 1232 deletions
+388
View File
@@ -0,0 +1,388 @@
# Token 自动管理 - 测试说明
## 修改摘要
### 目标
统一 Token 管理机制,**完全依赖自动获取**,移除所有手动传递 token 的代码。
### 架构说明
#### 1. **客户端请求**axios 拦截器自动处理)
在**浏览器环境**中,所有使用 `axiosInstance` 的请求都会被 axios 拦截器自动添加 token
**axios 拦截器** (`app/api/axios-client.ts:66-86`)
```typescript
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;
});
```
**受益的请求类型**
- 客户端组件中的 `useEffect``useCallback`、事件处理函数中的所有 API 请求
- 所有 PostgREST 请求(`postgrestGet`, `postgrestPost`, `postgrestPut`, `postgrestDelete`
- 所有使用 `apiRequest``downloadFile` 的请求
#### 1.5. **服务端请求**(需要显式传递 token)
**Remix Loader/Action(服务端)** 中,`typeof window === 'undefined'`,axios 拦截器**不会**自动添加 token。
**解决方案**:从 Cookie Session 获取 `frontendJWT` 并显式传递给 API 函数
**示例** (`app/routes/rules-files.tsx:62-74`)
```typescript
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`)
```typescript
// 设置请求头
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` 获取 `frontendJWT`line 65
- ✅ 传递给 `getDocumentTypes`line 74
**客户端组件修改**
- ❌ 移除从 `useLoaderData` 中读取 `frontendJWT`
- ❌ 移除所有客户端 API 调用中的 token 参数
- ✅ 完全依赖 axios 拦截器自动添加 token
#### 1.5. `app/routes/documents.list.tsx`
**Loader 修改**
- ✅ 从 `getUserSession` 获取 `frontendJWT`line 44
- ✅ 传递给 `getDocumentTypes`line 52
**客户端组件修改**
- ❌ 移除所有客户端 API 调用中的 token 参数
- ✅ 完全依赖 axios 拦截器自动添加 token
#### 2. `app/api/evaluation_points/rules-files.ts`
**移除的内容**
-`DocumentSearchParams.token` 字段
-`getReviewFiles` 函数的 `token` 参数处理
-`updateDocumentAuditStatus` 函数的 `token` 参数
- ❌ 所有 `postgrestPost`/`postgrestPut` 调用中的 token 参数
**关键修改**
```typescript
// ❌ 修改前
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`
**修改的内容**
- ❌ 移除 `appendContractAttachments``jwtToken` 参数
- ✅ 添加从 localStorage 获取 token 的逻辑(因为使用 fetch,不经过 axios 拦截器)
**关键修改**
```typescript
// ❌ 修改前
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_token``user_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_details``count_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 Session`getUserSession`
- 客户端组件使用 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
-**客户端组件无需传递 token**axios 拦截器自动处理)
-**API 函数保留 optional token 参数**(向后兼容)
**下一步建议**
- 考虑将所有使用 fetch 的 API 改为使用 axios,统一请求方式
- 统一 loader 中的错误处理模式