Files
ephron-ren-prd/prd-prompt-test-csrf-initialization-fix.md

508 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Prompt 详情页测试功能安全令牌未初始化问题修复 PRD
> **版本**: v1.0
> **日期**: 2026-05-25
> **状态**: 📝 待评审
---
## 一、背景
在 prompt 服务的公开提示词详情页中,页面提供了“测试”标签,允许已登录用户直接选择模型并运行提示词测试。
目标页面示例:
- `https://prompt.ephron.ren/prompts/prompt-20260508110413`
按预期:
- 用户只要处于已登录状态
- 打开公开详情页
- 进入“测试”标签并点击“开始测试”
就应当可以直接完成测试。
但现状是:即使用户已经登录,测试功能仍可能提示:
- `安全令牌未初始化,请刷新页面后重试`
- 或在接口层表现为 `CSRF token 验证失败`
这说明“已登录”与“测试功能可用”之间还存在一个未被详情页自身完成的初始化前置条件。
---
## 二、问题现象
### 2.1 用户侧现象
在已登录状态下,用户访问公开提示词详情页:
1. 打开 `/prompts/{key}` 页面
2. 切换到“测试”标签
3. 选择模型
4. 输入测试内容
5. 点击“开始测试”
**实际结果:**
- 页面提示安全令牌未初始化,或请求失败
- 后端接口返回 403错误为 `CSRF token 验证失败`
### 2.2 旁证现象
如果用户先访问某些后台页面(例如 `/admin/settings`),再回到同一个公开提示词详情页重复操作,测试又可能恢复正常。
这说明:
- 模型能力本身不是根因
- 登录态本身也不是根因
- 更像是某个与页面测试功能相关的 token / cookie 初始化,只在后台页面中发生,而在公开详情页中未发生
---
## 三、影响范围
### 3.1 直接影响
- 所有挂载了“测试”面板的公开提示词详情页:`/prompts/{key}`
- 所有“已登录但首次直接从公开详情页发起测试”的用户
### 3.2 间接影响
- 用户会误以为账号失效、会话异常或模型配置有问题
- 测试功能表现为“不稳定”:访问过后台页面后又恢复,增加排查成本
- 页面对后台页面存在隐式依赖,破坏公开详情页的独立性
### 3.3 不受影响的路径
- 仅浏览提示词正文的用户
- 不使用测试功能的用户
- 已经访问过会初始化 CSRF cookie 的后台页面,且当前 cookie 尚未过期的用户
---
## 四、现状与源码定位
### 4.1 公开详情页会直接挂载测试组件
文件:`prompt/templates/public/detail.html`
详情页模板中直接加载测试脚本并实例化 `PromptTester`
- 加载 `/static/js/test-prompt.js`
- `new PromptTester({...})`
这意味着:
- 公开详情页自身就是测试功能的承载页面
- 测试所需前置条件应由该页面或其服务端路由负责准备
### 4.2 前端测试逻辑直接从 cookie 中读取 `ephron_csrf`
文件:`prompt/static/js/test-prompt.js`
核心逻辑:
```javascript
const csrfToken = document.cookie.match(/ephron_csrf=([^;]+)/)?.[1] || '';
const response = await fetch(`/api/prompts/${this.promptKey}/test`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify(body),
});
```
可见前端默认假设:
- `document.cookie` 中一定已经存在 `ephron_csrf`
若该 cookie 不存在,则会把空字符串作为 `X-CSRF-Token` 发给后端。
### 4.3 测试 API 后端强制校验 `ephron_auth + ephron_csrf`
文件:`prompt/src/routes/api.py`
关键逻辑:
```python
token = request.cookies.get("ephron_auth")
user = get_auth_user(token)
if not user:
raise HTTPException(status_code=401, detail="需要登录")
csrf_cookie = request.cookies.get("ephron_csrf")
csrf_header = request.headers.get("X-CSRF-Token")
if not verify_csrf_token(csrf_header, csrf_cookie):
raise HTTPException(status_code=403, detail="CSRF token 验证失败")
```
说明测试接口要求同时满足:
1. 用户已登录(`ephron_auth` 有效)
2. 浏览器中存在 `ephron_csrf`
3. 请求头中的 `X-CSRF-Token` 与 cookie 中的 token 一致
### 4.4 后台页面会主动初始化 CSRF cookie
文件:`prompt/src/routes/admin.py`
后台多个 GET 页面采用统一模式:
```python
csrf_token = generate_csrf_token()
response = templates.TemplateResponse(...)
set_csrf_cookie(response, csrf_token, is_development=IS_DEVELOPMENT)
return response
```
说明项目当前已有成熟的 CSRF 初始化机制:
- 生成 token
- 写入 `ephron_csrf` cookie
- 供表单和前端 JS 后续读取
### 4.5 公开详情页路由未承担相同职责
结合现有 PRD 记录与本次源码排查,可以确认公开详情页路由:
文件:`prompt/src/routes/pages.py`
在渲染 `public/detail.html` 时:
- 直接返回 `TemplateResponse`
- 没有看到对 `generate_csrf_token()` 的调用
- 没有看到对 `set_csrf_cookie(...)` 的调用
这与后台页面形成明显差异。
---
## 五、根因分析
### 5.1 根因结论
> **公开提示词详情页提供了测试能力,但没有初始化该能力所依赖的 CSRF token cookie。**
### 5.2 具体链路
当前链路可还原为:
1. 用户在 `auth.ephron.ren` 完成登录
2. 浏览器具备有效的 `ephron_auth`
3. 用户直接进入 `prompt.ephron.ren/prompts/{key}`
4. 详情页渲染时没有种下 `ephron_csrf`
5. 用户点击“开始测试”
6. `test-prompt.js``document.cookie` 读取不到 `ephron_csrf`
7. 前端仍继续向 `/api/prompts/{key}/test` 发送请求,`X-CSRF-Token` 为空
8. 后端发现 `csrf_header``csrf_cookie` 缺失或不一致
9. 返回 `403 CSRF token 验证失败`
10. 前端再将其表现为“安全令牌未初始化”类错误提示
### 5.3 为什么访问 `/admin/settings` 后会“恢复”
因为后台设置页会调用:
- `generate_csrf_token()`
- `set_csrf_cookie(...)`
于是浏览器终于具备了 `ephron_csrf`。之后再回到详情页,同一份前端 JS 就能成功从 `document.cookie` 中读到 token于是测试恢复正常。
这进一步证明:
- 问题不是模型请求本身
- 问题不是账号鉴权本身
- 问题是 **公开详情页缺失 CSRF 初始化责任**
---
## 六、修复目标
1. 已登录用户首次直接访问公开提示词详情页时,可直接使用测试功能
2. 测试功能不再依赖用户事先访问后台页面进行“预热”
3. 保持现有 CSRF 防护机制,不通过移除校验来规避问题
4. 前端在 token 缺失时给出清晰、可理解的错误提示
5. 为该问题补齐自动化回归测试,避免未来再次回归
---
## 七、修复方案
## 7.1 方案 A公开详情页 GET 路由补发 CSRF cookie主修复必须做
文件:`prompt/src/routes/pages.py`
在渲染 `public/detail.html` 的公开详情页路由中,补齐与后台页面一致的初始化逻辑:
1. 调用 `generate_csrf_token()` 生成 token
2. 使用 `TemplateResponse` 先构造响应对象
3. 调用 `set_csrf_cookie(response, csrf_token, is_development=IS_DEVELOPMENT)`
4. 返回该 response
### 设计要求
- 实现方式应尽量复用后台页面已有模式
- 不新增新的 CSRF 机制分支
- 不改动接口鉴权模型
- 不改动测试 API 的 CSRF 校验规则
### 预期效果
- 用户打开详情页即自动获得 `ephron_csrf`
- 点击测试时,前端能直接从 cookie 取到有效 token
- 后端的现有校验链路无需修改即可通过
---
## 7.2 方案 B前端增加 token 缺失的显式防御(兜底增强,建议一并做)
文件:`prompt/static/js/test-prompt.js`
在发起 `fetch()` 前增加前置判断:
```javascript
const csrfToken = document.cookie.match(/ephron_csrf=([^;]+)/)?.[1] || '';
if (!csrfToken) {
throw new Error('安全令牌未初始化,请刷新页面后重试');
}
```
### 目的
- 避免把空 token 静默发给后端
- 让用户看到更明确的错误原因
- 即使未来某次页面初始化异常,也能更快暴露问题归因
### 说明
这不是根修复,只是防御性增强。真正的根因修复仍然是方案 A。
---
## 7.3 本次不建议采用的替代方案
### 替代方案:新增单独的 CSRF 初始化 API
例如新增:
- `GET /api/auth/csrf`
并在前端缺 token 时先请求该接口。
### 不作为首选原因
- 当前项目已有稳定的服务端渲染页面 + `set_csrf_cookie` 模式
- 新增 API 会引入额外异步初始化链路和更多状态分支
- 对当前问题来说属于过度设计
除非后续要支持更多纯前端异步挂载场景,否则现阶段没有必要。
---
## 八、实现清单
### 8.1 后端改动
文件:`prompt/src/routes/pages.py`
#### 需要做的事
- 导入:
- `generate_csrf_token`
- `set_csrf_cookie`
-`prompt_detail()` 中:
- 生成 `csrf_token`
- 将原本直接返回的 `TemplateResponse` 改为先赋值给 `response`
- 调用 `set_csrf_cookie(response, csrf_token, is_development=IS_DEVELOPMENT)`
- 返回 `response`
#### 约束
- 不改动详情页已有模板参数结构,避免影响正文渲染
- 不需要将 token 注入模板 DOMcookie 已足够供当前 JS 使用
### 8.2 前端改动
文件:`prompt/static/js/test-prompt.js`
#### 需要做的事
-`runTest()` 中的 `fetch()` 前增加 token 非空校验
- 优化错误提示映射:
- `401` → 需要登录后才能测试
- `403` 且 detail 含 CSRF → 安全令牌失效或未初始化,请刷新页面后重试
- 其他错误 → 保持现有通用错误兜底
#### 约束
- 不改变现有 SSE 流式处理逻辑
- 不改变现有模型选择、变量注入、Markdown 渲染逻辑
---
## 九、测试方案
## 9.1 自动化测试补齐
### 测试 1公开详情页 GET 会设置 `ephron_csrf`
建议位置:
- `prompt/tests/test_prompt_service_api.py`
- 或更合适的 pages 路由测试文件
#### 步骤
1. 创建一个 active prompt
2. 构造已登录 client
3. `GET /prompts/{key}`
4. 断言 response / client cookies 中存在 `ephron_csrf`
#### 验证点
- 详情页自身承担了 CSRF 初始化职责
### 测试 2首次访问详情页后可直接调用测试接口
#### 步骤
1. 构造已登录 client
2. `GET /prompts/{key}`
3. 从 client cookies 读取 `ephron_csrf`
4. 直接 `POST /api/prompts/{key}/test`
5. 请求头中传相同 token
6. 断言不返回 `403 CSRF token 验证失败`
#### 验证点
- 详情页访问一次后,测试能力可以独立工作
### 测试 3未登录用户仍被正确拦截
#### 步骤
1. 不注入 `ephron_auth`
2. 请求 `/api/prompts/{key}/test`
3. 断言返回 `401 需要登录`
#### 验证点
- 修复不应削弱登录鉴权
### 测试 4缺失 token 时仍正确返回 CSRF 错误
#### 步骤
1. 构造已登录 client
2. 不提供 `ephron_csrf` 或提供空 header
3. 调用测试接口
4. 断言返回 `403`
#### 验证点
- 修复不应通过绕过 CSRF 校验来“修好”功能
---
## 9.2 手工验收
### 场景 A首次直接测试
1. 使用已登录账号登录
2. 直接访问任意公开提示词详情页
3. 不访问任何 `/admin/*` 页面
4. 切到“测试”标签
5. 输入内容后点击“开始测试”
**预期:**
- 请求成功
- 页面正常流式输出结果
- 不再出现“安全令牌未初始化”或 `CSRF token 验证失败`
### 场景 B未登录测试
1. 清除登录态
2. 打开公开详情页
3. 执行测试
**预期:**
- 页面提示需要登录
- 后端返回 401
### 场景 C异常 token 场景
1. 手动清除或伪造 `ephron_csrf`
2. 保留登录态
3. 再次点击测试
**预期:**
- 页面出现清晰的 token 失效/未初始化提示
- 不应只显示模糊的“请求失败”
---
## 十、验收标准
满足以下条件才算本 PRD 完成:
1. 公开提示词详情页首次加载时即可写入 `ephron_csrf`
2. 已登录用户无需访问后台页面即可直接使用测试功能
3. `/api/prompts/{key}/test` 仍保留现有 CSRF 校验机制
4. 未登录用户仍返回 401不放宽权限
5. 前端在 token 缺失时给出明确错误提示
6. 自动化测试覆盖“详情页初始化 token”的集成场景
7. 不引入对正文展示、示例展示、模型下拉和测试流式输出的回归问题
---
## 十一、风险与注意事项
### 11.1 不要用“删除 CSRF 校验”来修问题
这是最需要避免的误修方向。问题的本质是:
- token 没初始化
而不是:
- token 校验太严格
### 11.2 注意 cookie 域与 secure 配置
`set_csrf_cookie()` 的行为受共享配置影响。上线时需确认:
- `CSRF_COOKIE_DOMAIN`
- `secure`
- `samesite`
`prompt.ephron.ren` 当前部署环境兼容。
### 11.3 不要让公开详情页继续隐式依赖后台页
本次修复的核心目标之一,就是消除:
- “访问过 `/admin/settings` 才能测试成功”
这种不合理依赖关系。
---
## 十二、结论
本问题的本质不是账号错误,也不是模型配置错误,而是:
> **公开提示词详情页提供了测试能力,却没有初始化该能力所需的 CSRF cookie。**
因此用户即便已经登录,测试功能仍然会失败。正确修复方式应是:
- 由公开详情页 GET 路由主动完成 CSRF 初始化
- 前端增加 token 缺失的显式兜底提示
- 保持后端现有安全校验不变
这是一次典型的“能力挂载页未承担其初始化责任”的问题,修复后应确保详情页测试能力真正独立、自洽、可首次即用。