spark-server/docs/sms.md
2026-09-22 17:39:38 +08:00

4.4 KiB
Raw Blame History

验证码(SMS 模块)

邮箱与手机 OTP 统一走 POST /v1/sms/codes。校验在登录 / 注销等业务里调用 sms logic,不单独暴露校验 HTTP。

OpenAPI:GET /openapi.yaml → /v1/sms/codes。错误码目录:/docs/errors(限流 42902)。


发码接口

项 规则
路径 POST /v1/sms/codes
登录 无需 Bearer
注销(purpose=2) 需要 Bearer;target 必须是当前账号绑定邮箱(不支持向手机号发注销码)
Header X-Language 必填(zh-CN / en / zh-TW),用于邮件主题/正文;非法或缺失 → 40001
Body purpose:1=登录,2=注销;target:邮箱或大陆 11 位手机号(可带 +86),服务端识别,不要再传 channel
成功 data {"expires_in":300} — 验证码有效秒数(固定 300,与 Redis TTL 一致)

网关须写入 X-Real-IP(或 PROXY 解析后的等价头)。限流键用 reqctx.RealIP,不用 gnet RemoteAddr(C6)。


封控规则(两层,都会返回 42902)

客户端应:同一 target 至少间隔 60s 再点「发送」;并避免同一公网 IP 在短时间内狂刷登录/发码/刷新。

1. HTTP 层:按客户端 IP 令牌桶

项 值
中间件 AuthRateLimit(在 Auth 之前)
限流键 RealIP(空则记为 unknown)
算法 每 IP 一个令牌桶:rate = burst = auth_per_second
配置 rate_limit.auth_per_second(configs/config.yaml,默认 5)
命中路径 POST /v1/sms/codes、POST /v1/auth/login、POST /v1/auth/token/refresh(三条共用同一 IP 桶)
响应 HTTP 200,code 42902,msg 随语言(如「请求过于频繁」/ rate limited)

说明:这是进程内限流,多实例各自计数;前面 Nginx 仍可再加 WAF / 全局限流。

2. 业务层:按「渠道 + 用途 + 目标」发送冷却

项 值
冷却时长 60 秒(代码常量 defaultCodeCooldownSeconds,当前不可 YAML 配置)
粒度 规范化后的 target + purpose + 渠道(邮箱 / 手机各算一条)
存储键(逻辑) {channel}:{purpose}:{target},例如 1:1:ada@example.com
Redis 冷却:SETNX sms:cd:{逻辑键} TTL 60s;验证码:sms:code:{逻辑键} TTL 300s
行为 60s 内对同一 target+purpose+渠道再次发码 → 42902(不覆盖未过期的码)
发送失败 SMTP / 阿里云返回错误时会 Del 该逻辑键(含冷却),可立即重试(仍受 IP 桶约束)

独立计数示例(互不影响冷却):

  • purpose=1 与 purpose=2 各 60s 一条;
  • 同一邮箱与同一手机号是不同 channel;
  • 不同邮箱 / 不同手机号各自 60s。

客户端建议

场景 建议
UI 倒计时 以 60s 为准(与业务冷却一致);expires_in=300 只表示码多久失效
收到 42902 不要立刻连点;至少等待 60s 或提示用户稍后再试
多 Tab / 多设备 同一 target 共享冷却;不同 IP 仍共享 target 冷却

验证码本身

项 规则
长度 6 位数字
有效期 300s(5 分钟)
校验 登录 / 注销等业务调用 CheckAndConsume:正确则删除(一次性);错误 / 过期 / 已用过 → 40103(统一「验证码无效」)
校验限流 无单独「输错 N 次锁定」;仍受登录等接口的 IP 桶约束

测试与白名单(不走真实 SMTP/短信)

类型 规则
邮箱 test01@spark.com~test10@spark.com:Send 不发邮件、不写 Redis 冷却;登录 / 注销校验 任意 verify_code
手机 +8613800138000~+8613800138002:不发短信;校验任意码(与测试邮箱类似)

配置速查

配置项 作用
rate_limit.auth_per_second IP 桶速率(默认 5/s,与 login / refresh / 发码共用)
sms.smtp.* 邮箱 OTP(DirectMail SMTP)
sms.aliyun_sms.* 手机 OTP(按运营商模板)
Redis 生产存 OTP + 冷却;无 Redis 时进程内 MemoryStore(单实例开发)

OTP 的 60s 冷却 与 300s 有效期 在 internal/module/sms/logic/service.go 常量中;若要运营可调,需改代码并重新发布。