spark-server/docs/sms.md

98 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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