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

13 KiB
Raw Permalink Blame History

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({...})

这意味着:

  • 公开详情页自身就是测试功能的承载页面
  • 测试所需前置条件应由该页面或其服务端路由负责准备

文件:prompt/static/js/test-prompt.js

核心逻辑:

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

关键逻辑:

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 一致

文件:prompt/src/routes/admin.py

后台多个 GET 页面采用统一模式:

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.jsdocument.cookie 读取不到 ephron_csrf
  7. 前端仍继续向 /api/prompts/{key}/test 发送请求,X-CSRF-Token 为空
  8. 后端发现 csrf_headercsrf_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() 前增加前置判断:

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 校验太严格

set_csrf_cookie() 的行为受共享配置影响。上线时需确认:

  • CSRF_COOKIE_DOMAIN
  • secure
  • samesite

prompt.ephron.ren 当前部署环境兼容。

11.3 不要让公开详情页继续隐式依赖后台页

本次修复的核心目标之一,就是消除:

  • “访问过 /admin/settings 才能测试成功”

这种不合理依赖关系。


十二、结论

本问题的本质不是账号错误,也不是模型配置错误,而是:

公开提示词详情页提供了测试能力,却没有初始化该能力所需的 CSRF cookie。

因此用户即便已经登录,测试功能仍然会失败。正确修复方式应是:

  • 由公开详情页 GET 路由主动完成 CSRF 初始化
  • 前端增加 token 缺失的显式兜底提示
  • 保持后端现有安全校验不变

这是一次典型的“能力挂载页未承担其初始化责任”的问题,修复后应确保详情页测试能力真正独立、自洽、可首次即用。