Browse Source

docs: add auth captcha design spec (2026-04-19)

Made-with: Cursor
main
npmrun 7 hours ago
parent
commit
a0da55b499
  1. 162
      docs/superpowers/specs/2026-04-19-auth-captcha-design.md

162
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)` 作为 `<img src>`(避免额外 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 或数据库。
Loading…
Cancel
Save