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
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 用户侧流程(登录页与注册页对称)
- 页面加载后请求验证码接口,展示图片与「换一张」。
- 用户填写用户名、密码、验证码字符后提交。
POST /api/auth/login或POST /api/auth/register的请求体在原有字段基础上增加captchaId、captchaAnswer。- 服务端 先限流,再 校验验证码;失败则立即返回,不执行密码校验或用户创建。
- 验证码通过后 删除 该 challenge,再执行现有
loginUser/registerUser。
3.2 服务端状态
- Challenge 存储:进程内结构,
captchaId → { 答案规范化形式或安全比较所需数据、过期时间 }。 - 建议 TTL:3 分钟(实现可为可配置常量,默认 180000 ms)。
- 过期清理:在
create/consume时惰性删除过期项;可选轻量定时清扫,防止 Map 无限增长。
3.3 注册关闭时的顺序
当全局配置 allowRegister === false 时,POST /api/auth/register 的处理顺序为:
- 限流
- 验证码校验(与登录相同要求)
- 再返回 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
请求体(在原有字段基础上增加):
{
"username": "demo_user",
"password": "123456",
"captchaId": "server-issued-id",
"captchaAnswer": "user-input"
}
处理顺序:
authRateLimit(登录与注册可分桶或共用,实现计划定稿)- 校验
captchaId/captchaAnswer存在且consume成功 - 现有参数校验与
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 或数据库。