1 changed files with 162 additions and 0 deletions
@ -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…
Reference in new issue