Skip to content

验证码状态机(captcha + sms-code) ​

图形验证码(Redis: oauth:captcha:{id}, TTL 120s) ​

mermaid
stateDiagram-v2
    [*] --> Fresh: captcha() 返回
    Fresh --> Consumed: sms-send 到达(无论图形码对错)
    Fresh --> Expired: 120s 无消费 / 生成 IP 的 /24 段变更后校验
    Consumed --> [*]
    Expired --> [*]

前端对照:

  • captcha 调用后立即渲染 <image :src="png">
  • 任何失败分支(40101003 / 40101004)后必须重新 captcha
  • 换网(WiFi↔4G)IP /24 段变更 → 已生成但未消费的 captcha 会失效

短信验证码(Redis: oauth:sms:code:{phone}:{clientId}, TTL 600s) ​

mermaid
stateDiagram-v2
    [*] --> Fresh: sms-send 成功
    Fresh --> Fresh: sms-verify/sms-login 码正确(verify 不消费;login 消费)
    Fresh --> Wrong: 码错误(attempts+1)
    Wrong --> Fresh: attempts < 3(保留剩余 TTL)
    Wrong --> [*]: 第 3 次错误 → 立即删除
    Fresh --> Consumed: sms-login 成功 / bind-phone 成功
    Fresh --> Expired: 600s 过期
    Consumed --> [*]
    Expired --> [*]

前端对照(三个独立约束):

独立约束UI 对应
60s 发送倒计时(限流)按钮倒计时禁用
10min 有效期(TTL)"验证码已过期"
3 错锁定(attempts)"剩余 X 次"提示

三层独立约束的 UI 实现 ​

约束 1:60s 发送倒计时(限流) ​

  • 服务端硬限:手机号 60s 内只能发送 1 条
  • 前端可先 disable 按钮 60s(体验优化),但最终以服务端为准
  • 超限返回 40101005(msg 含「60s」),前端展示服务端 msg

约束 2:10min 有效期(TTL) ​

  • 服务端 Redis TTL 600s
  • 用户 10 分钟内未使用验证码 → 过期
  • 任意时刻 sms-verify / sms-login 拿到 40101001(码不存在) → 引导重发

约束 3:3 错锁定(attempts) ​

  • 错误计数器跨 sms-verify 与 sms-login 共享(同一个 Redis key)
  • 第 1、2 次错:40101003(msg=「短信验证码错误」),可继续试
  • 第 3 次错:码被立即删除,下次请求返回 40101001(码不存在),必须重发
  • 分步表单特别提示:如果预验证已经输错 2 次,最终提交只剩 1 次机会

验证码生命周期与并发安全 ​

服务端用 Lua 脚本保证 GET + 校验 + 标记 consumed 的原子性:

lua
-- captcha consume 原子脚本(节选)
local data = redis.call('GET', KEYS[1])
if not data then return {0, 'not_found'} end
local obj = cjson.decode(data)
if obj.state ~= 'fresh' then return {0, 'consumed'} end
if obj.ip_subnet ~= ARGV[2] then return {0, 'ip_mismatch'} end
if obj.code_hash ~= ARGV[1] then return {0, 'wrong'} end
obj.state = 'consumed'
redis.call('SET', KEYS[1], cjson.encode(obj), 'EX', tonumber(ARGV[3]))
return {1, 'ok'}

前端不要本地缓存 captcha —— 并发安全由服务端 Lua 保证,前端重复调用会浪费服务端计算资源。


跨设备 captcha 失效场景 ​

场景 1:换网(WiFi ↔ 4G) ​

  • 用户的 IP /24 段变化(如从 192.168.1.0/24 到 10.0.0.0/24)
  • 已生成但未消费的 captcha 校验时 ip_mismatch → 静默失败
  • 用户无感知(前端用前端提示),需主动重新拉 captcha

场景 2:captcha 跨页面共享 ​

  • 同一 (IP /24, captchaId) 一次性消费
  • 跨页面复用会触发 consumed 状态 → captcha wrong
  • 每个新页面/失败分支都必须重新 captcha

真机调试要点 ​

现象排查
一直提示图形码错(实际码正确)检查是否换网 / 多次点击 / captcha 已被前一次消费
短信码一直显示「不存在」检查 clientId 是否与发码时一致 / 600s TTL 是否已过 / 错码累计 3 次
60s 后还能立即发送服务端 Redis 重启或限流配置丢失;检查 oauth:rate:sms:phone:{phone}
提示「图形验证码已过期」120s 内未消费 / 用户停留页面过久

mock vs 生产差异 ​

项mock(开发)生产
短信码获取固定 1234(启动日志可见)真短信
sms-send 图形码校验+一次性消费(内存 Map)✅ Redis + IP /24 绑定
限流仅手机号 60s/1全 5 层(IP 30/min / 日 10 条 仅生产)
短信码 TTL600s600s
captcha IP /24 绑定❌ 不绑定✅ 绑定

联调提示:mock 端拿到的是模拟 IP,生产必须真机(不同网络)验证 IP /24 段变更。