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