diff --git a/docs/superpowers/specs/2026-04-19-auth-captcha-design.md b/docs/superpowers/specs/2026-04-19-auth-captcha-design.md new file mode 100644 index 0000000..a5bca35 --- /dev/null +++ b/docs/superpowers/specs/2026-04-19-auth-captcha-design.md @@ -0,0 +1,162 @@ +# 登录与注册图形验证码设计文档 + +日期:2026-04-19 +范围:在现有用户名密码登录/注册流程上增加**自建图形验证码**与**内存限流**,不引入境外第三方人机验证服务(如 Turnstile、hCaptcha)。 +与 [2026-04-16-username-password-auth-design.md](./2026-04-16-username-password-auth-design.md) 的关系:该文档「明确不做 · 验证码」由**本文档覆盖**;会话、Cookie、错误策略等仍以 04-16 文档为准,除非本文另有说明。 + +## 1. 背景与目标 + +### 1.1 动机 + +- 缓解针对 **登录** 的撞库/暴力尝试与针对 **注册** 的批量自动注册。 +- 约束:**少依赖境外第三方**;验证码形态为 **服务端生成的图形/字符码**;部署假设为 **单机单实例**。 + +### 1.2 成功标准 + +- 登录、注册请求在执行业务逻辑前必须通过验证码校验。 +- 验证码 **一次性**:校验成功或失败后按本文规则失效,不可复用。 +- 对自动化工具提高成本;对正常用户仅增加一步输入与可选「换一张」。 +- 单机内存方案可接受:进程重启后未使用验证码失效;限流计数重启后清零。 + +## 2. 非目标 + +- 接入 Cloudflare Turnstile、hCaptcha、reCAPTCHA 等第三方服务。 +- 多实例水平扩展下的共享验证码存储(若未来扩展,需另案改为 Redis/DB)。 +- 首版无障碍替代方案(如语音验证码);可作为后续 backlog。 + +## 3. 总体架构与流程 + +### 3.1 用户侧流程(登录页与注册页对称) + +1. 页面加载后请求验证码接口,展示图片与「换一张」。 +2. 用户填写用户名、密码、验证码字符后提交。 +3. `POST /api/auth/login` 或 `POST /api/auth/register` 的请求体在原有字段基础上增加 `captchaId`、`captchaAnswer`。 +4. 服务端 **先限流**,再 **校验验证码**;失败则立即返回,**不**执行密码校验或用户创建。 +5. 验证码通过后 **删除** 该 challenge,再执行现有 `loginUser` / `registerUser`。 + +### 3.2 服务端状态 + +- **Challenge 存储**:进程内结构,`captchaId → { 答案规范化形式或安全比较所需数据、过期时间 }`。 +- **建议 TTL**:3 分钟(实现可为可配置常量,默认 180000 ms)。 +- **过期清理**:在 `create` / `consume` 时惰性删除过期项;可选轻量定时清扫,防止 Map 无限增长。 + +### 3.3 注册关闭时的顺序 + +当全局配置 `allowRegister === false` 时,`POST /api/auth/register` 的处理顺序为: + +1. 限流 +2. 验证码校验(与登录相同要求) +3. 再返回 **403**「当前已关闭注册」 + +避免在未校验验证码的情况下暴露可滥用路径的语义分歧;成本上攻击者仍需解题。 + +## 4. 安全规则 + +### 4.1 验证码消费规则 + +- **校验成功**:删除该 `captchaId`,继续后续业务。 +- **校验失败**(答案错误、格式非法):**删除**该 `captchaId`,防止对同一图片暴力枚举答案。 +- **不存在或已过期**:返回与「错误」统一的对外文案,日志可细分原因。 + +### 4.2 答案规范化(须在实现中写死) + +- 对用户输入:`trim` 后 **转小写**(假定验证码字符集仅为 `a-z` 与 `0-9`,长度 4~6,具体长度与字符集为可配置常量)。 +- 服务端生成时与存储比较逻辑一致。 + +### 4.3 比较方式 + +- 使用 `crypto.timingSafeEqual`(或等价的常量时间比较)比较字节/哈希,避免时序旁路;若长度不一致应先失败且不泄漏细节。 + +### 4.4 登录错误与隐私 + +- 验证码错误:**不**改变现有登录「认证失败」的统一对外表现策略;验证码错误单独使用 **400** + 明确文案(如「验证码错误或已过期,请重试」)。 +- 登录 **用户名/密码错误** 仍走现有 `authFailedError` 等行为,**不**因验证码而区分用户是否存在。 + +### 4.5 传输与缓存 + +- 验证码获取接口响应须包含 **`Cache-Control: no-store`**。 + +### 4.6 客户端 IP(限流键) + +- 与项目现有对 `event`、反向代理的约定一致;须在实现计划中写明单一策略(例如:存在可信 `X-Forwarded-For` 时取第一段,否则 `socket` 远端地址)。生产环境应在网关层限制可伪造头。 + +## 5. 组件与接口 + +### 5.1 服务端模块 + +| 模块 | 职责 | +|------|------| +| `captchaStore` | `create`、`consume`、惰性过期清理 | +| `captchaImage` | 生成图形/字符(优先无原生编译依赖的实现,例如 SVG 输出) | +| `authRateLimit` | 按 IP(及如需后续扩展:按路由分桶)在内存中计数 | + +### 5.2 `GET /api/auth/captcha` + +- **用途**:签发新 challenge 并返回可渲染图片数据。 +- **响应 JSON(建议)**: + - `captchaId`:字符串,客户端原样回传。 + - `imageSvg`:SVG 源码字符串;前端统一使用 `data:image/svg+xml;charset=utf-8,` + `encodeURIComponent(imageSvg)` 作为 ``(避免额外 Base64 编解码)。 +- **限流**:对该路径单独设上限(建议可配置,默认例如每 IP 每分钟 60 次),防止刷接口占满内存。 + +### 5.3 `POST /api/auth/login` / `POST /api/auth/register` + +**请求体(在原有字段基础上增加)**: + +```json +{ + "username": "demo_user", + "password": "123456", + "captchaId": "server-issued-id", + "captchaAnswer": "user-input" +} +``` + +**处理顺序**: + +1. `authRateLimit`(登录与注册可分桶或共用,实现计划定稿) +2. 校验 `captchaId` / `captchaAnswer` 存在且 `consume` 成功 +3. 现有参数校验与 `loginUser` / `registerUser`(注册含 `allowRegister` 检查) + +### 5.4 公开 API 清单 + +- 在 `server/utils/auth-api-routes.ts`(或等价集中配置)中为 **`GET /api/auth/captcha`** 增加 allowlist,使未登录用户可获取验证码。 + +### 5.5 前端 + +- **页面**:`app/pages/login/index.vue`、`app/pages/register/index.vue`。 +- **行为**:加载时拉取验证码;「换一张」重新请求并清空验证码输入;**提交失败后**必须重新拉取验证码。 +- **字段**:提交时附带 `captchaId`、`captchaAnswer`。 + +## 6. 错误与 HTTP 状态 + +| 场景 | 建议状态码 | 用户可见文案(示例) | +|------|------------|----------------------| +| 缺少 `captchaId` / `captchaAnswer` | 400 | 参数错误(与现有风格一致) | +| 验证码错误、过期、不存在(对外统一) | 400 | 验证码错误或已过期,请重试 | +| 限流 | 429 | 请求过于频繁,请稍后再试(可选 `Retry-After`) | +| 注册关闭 | 403 | 当前已关闭注册(在验证码通过之后返回) | +| 登录用户名/密码错误 | 保持现有 | 保持现有 | + +## 7. 测试与验收 + +### 7.1 单元测试 + +- `captchaStore`:`consume` 成功路径;错误答案导致 id 删除;过期拒绝;已消费 id 再次使用失败。 +- `authRateLimit`:窗口内超限返回限流结果。 + +### 7.2 接口测试 + +- `GET /api/auth/captcha`:结构合法、`Cache-Control: no-store`。 +- `POST login`:缺验证码字段 → 400;错误验证码 → 400 且未设置 session cookie;正确验证码 + 错误密码 → 现有登录失败行为。 +- `POST register`:`allowRegister === false` 时,验证码通过后返回 403。 + +### 7.3 手工验收 + +- 两页刷图、换一张、成功登录/注册。 +- 调低限流阈值验证 429。 +- 重启进程后旧 `captchaId` 提交失败(预期行为)。 + +## 8. 后续可选(不在首版范围) + +- 无障碍替代(音频或算术题切换)。 +- 多实例部署时将 challenge 与限流迁移至 Redis 或数据库。