# 验证码(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`~`test100@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` 常量中;若要运营可调,需改代码并重新发布。