You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

7.5 KiB

登录与注册图形验证码设计文档

日期:2026-04-19
范围:在现有用户名密码登录/注册流程上增加自建图形验证码内存限流,不引入境外第三方人机验证服务(如 Turnstile、hCaptcha)。
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/loginPOST /api/auth/register 的请求体在原有字段基础上增加 captchaIdcaptchaAnswer
  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-z0-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 createconsume、惰性过期清理
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

请求体(在原有字段基础上增加)

{
  "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.vueapp/pages/register/index.vue
  • 行为:加载时拉取验证码;「换一张」重新请求并清空验证码输入;提交失败后必须重新拉取验证码。
  • 字段:提交时附带 captchaIdcaptchaAnswer

6. 错误与 HTTP 状态

场景 建议状态码 用户可见文案(示例)
缺少 captchaId / captchaAnswer 400 参数错误(与现有风格一致)
验证码错误、过期、不存在(对外统一) 400 验证码错误或已过期,请重试
限流 429 请求过于频繁,请稍后再试(可选 Retry-After
注册关闭 403 当前已关闭注册(在验证码通过之后返回)
登录用户名/密码错误 保持现有 保持现有

7. 测试与验收

7.1 单元测试

  • captchaStoreconsume 成功路径;错误答案导致 id 删除;过期拒绝;已消费 id 再次使用失败。
  • authRateLimit:窗口内超限返回限流结果。

7.2 接口测试

  • GET /api/auth/captcha:结构合法、Cache-Control: no-store
  • POST login:缺验证码字段 → 400;错误验证码 → 400 且未设置 session cookie;正确验证码 + 错误密码 → 现有登录失败行为。
  • POST registerallowRegister === false 时,验证码通过后返回 403。

7.3 手工验收

  • 两页刷图、换一张、成功登录/注册。
  • 调低限流阈值验证 429。
  • 重启进程后旧 captchaId 提交失败(预期行为)。

8. 后续可选(不在首版范围)

  • 无障碍替代(音频或算术题切换)。
  • 多实例部署时将 challenge 与限流迁移至 Redis 或数据库。