feat: update sms docs
This commit is contained in:
parent
edec7d82e9
commit
b909cfbac0
@ -80,7 +80,7 @@ Public HTTP is `/v1/{resource}/...`. Register with Gin groups in `internal/trans
|
||||
|-------|--------|----------|
|
||||
| system | `/healthz`, `/readyz`, `/docs` | liveness, readiness, OpenAPI(`/docs` API,`/docs/enums` 枚举,`/docs/errors` 错误码) |
|
||||
| auth | `/v1/auth` | `POST /v1/auth/login`, `POST /v1/auth/token/refresh`, `POST /v1/auth/logout` |
|
||||
| sms | `/v1/sms` | `POST /v1/sms/codes`(`X-Language` 必填) |
|
||||
| sms | `/v1/sms` | `POST /v1/sms/codes`(`X-Language` 必填;封控见 `docs/sms.md`) |
|
||||
| users | `/v1/users` | `GET/PATCH/DELETE /v1/users/me` |
|
||||
| friends | `/v1/friends` | apply, requests, accept, profile, remark, pin, block, report;`GET /v1/friends/lookup?q=` |
|
||||
| groups | `/v1/groups` | create, profile, members, invite, kick, leave, transfer, dissolve |
|
||||
@ -96,6 +96,15 @@ Rules:
|
||||
- Noun resources, HTTP verbs for actions. No `/getUser`, `/sendMessage` under HTTP.
|
||||
- Login stays `POST /v1/auth/login` (create session). Do not put login under `/v1/users`.
|
||||
- Anonymous paths belong in `publicPath` (login, `POST /v1/sms/codes`, token refresh, healthz, readyz, docs, `GET /v1/meta/enums`).
|
||||
|
||||
### OTP send rate limits (`POST /v1/sms/codes`)
|
||||
|
||||
Documented in **`docs/sms.md`**. Implementation:
|
||||
|
||||
1. **`AuthRateLimit`** on `RealIP` for `/v1/sms/codes`, `/v1/auth/login`, `/v1/auth/token/refresh` — token bucket `rate_limit.auth_per_second` (default 5).
|
||||
2. **`sms.logic.Send`** — per `{channel}:{purpose}:{target}` **60s** cooldown via `CodeStore.SetCooldown` (`sms:cd:*` in Redis); OTP TTL **300s** (`sms:code:*`).
|
||||
|
||||
Both return **`errs.ErrRateLimited` (`42902`)**. UI countdown should follow **60s** target cooldown, not `expires_in`.
|
||||
- OpenAPI `tags` must match these groups. Docs jump tags `enums` / `errors` sit above `system` and open `/docs/enums`、`/docs/errors`.
|
||||
|
||||
## Checklist when adding an HTTP API
|
||||
|
||||
@ -4,6 +4,7 @@ Spark IM 服务端。Go 1.26 **模块化单体**(一台机器、一个进程
|
||||
|
||||
仓库:<https://gitea.wyrs365.com/Spark/spark-server.git>
|
||||
完整设计:[docs/architecture-v3.md](docs/architecture-v3.md)
|
||||
验证码发码 / 封控:[docs/sms.md](docs/sms.md)
|
||||
朋友圈(HTTP v1,无 TCP Push):[docs/moments.md](docs/moments.md)
|
||||
在线协议:`GET /docs`(Swagger)、`/docs/enums`、`/docs/errors`,规范 `GET /openapi.yaml`。
|
||||
|
||||
@ -94,13 +95,13 @@ HTTP 与 TCP **应答 Body** 同一套信封。业务成败只看 `code` 字符
|
||||
| Path | 资源 id:好友 uid、群号、MsgId |
|
||||
| Query | GET 筛选 / 分页 |
|
||||
|
||||
语言:`zh-CN`(缺省)/ `en` / `zh-TW`。网关写 `X-Real-IP`,客户端不要发。
|
||||
语言:`en`(缺省)/ `zh-CN` / `zh-TW`(`X-Language` 或 `Accept-Language`)。发码接口 **`X-Language` 必填**。网关写 `X-Real-IP`,客户端不要发。
|
||||
|
||||
---
|
||||
|
||||
## 登录
|
||||
|
||||
1. (可选)`POST /v1/sms/codes`,Header **`X-Language` 必填**,Body `{"purpose":1,"target":"…"}`。
|
||||
1. (可选)`POST /v1/sms/codes`,Header **`X-Language` 必填**,Body `{"purpose":1,"target":"…"}`。封控:**RealIP 令牌桶**(默认 5/s,与 login/refresh 共用)+ **同 target 60s 冷却**,均 `42902` — 见 [docs/sms.md](docs/sms.md)。
|
||||
2. `POST /v1/auth/login` 拿 `access_token` / `refresh_token`。`device_id` 由**客户端生成并持久化**,服务端不分配;写入 JWT `did`。
|
||||
3. 其它 HTTP:`Authorization: Bearer …`,可选 `X-Device-ID`(须等于登录时的 device_id)。
|
||||
4. 续期:`POST /v1/auth/token/refresh`,Body `{"refresh_token"}`,不要带 Bearer。
|
||||
@ -121,7 +122,8 @@ HTTP 与 TCP **应答 Body** 同一套信封。业务成败只看 `code` 字符
|
||||
| 探活 | `/healthz` `/readyz` | 存活;未就绪 HTTP 503 |
|
||||
| 文档 | `/docs` `/docs/enums` `/docs/errors` `/openapi.yaml` | 无需登录 |
|
||||
| 枚举 | `GET /v1/meta/enums` | 无需登录 |
|
||||
| 认证 | `/v1/auth` | 发码、登录、刷新、登出当前设备 |
|
||||
| 验证码 | `/v1/sms/codes` | 发码(封控见 [docs/sms.md](docs/sms.md)) |
|
||||
| 认证 | `/v1/auth` | 登录、刷新、登出当前设备 |
|
||||
| 用户 | `/v1/users/me` | 资料;`DELETE` 注销(迁入 `user_delete`) |
|
||||
| 好友 | `/v1/friends` | 申请、同意、查找(`GET /lookup?q=`)、备注、置顶、拉黑、举报 |
|
||||
| 群 | `/v1/groups` | 创建、资料、成员、邀请、踢人、转让、解散 |
|
||||
|
||||
97
docs/sms.md
Normal file
97
docs/sms.md
Normal file
@ -0,0 +1,97 @@
|
||||
# 验证码(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` 常量中;若要运营可调,需改代码并重新发布。
|
||||
@ -93,7 +93,7 @@ info:
|
||||
|
||||
### 调用顺序
|
||||
|
||||
1. (可选)`POST /v1/sms/codes` Header **`X-Language` 必填**(`zh-CN` / `en` / `zh-TW`),Body `{"purpose":1,"target":"邮箱或手机号"}`。服务端按格式识别邮箱/手机。测试号 `test01@spark.com`~`test10@spark.com` 可跳过发码。
|
||||
1. (可选)`POST /v1/sms/codes` Header **`X-Language` 必填**(`zh-CN` / `en` / `zh-TW`),Body `{"purpose":1,"target":"邮箱或手机号"}`。封控见 [docs/sms.md](../../../docs/sms.md)(IP 桶 + 同 target 60s 冷却,均 `42902`)。测试号 `test01@spark.com`~`test10@spark.com` 可跳过发码。
|
||||
2. `POST /v1/auth/login` Body 拿 `access_token` / `refresh_token`。`device_id` 由客户端生成并持久化,不要每次换。
|
||||
3. 其它 HTTP:Header `Authorization: Bearer …`,可选 `X-Device-ID`、`X-Language`。
|
||||
4. 续期:`POST /v1/auth/token/refresh`,**Body** `{"refresh_token"}`,不要带 Bearer。
|
||||
@ -510,10 +510,20 @@ paths:
|
||||
security: []
|
||||
summary: 发送验证码
|
||||
description: |
|
||||
Body:`purpose` 1=登录 2=注销;`target` 为邮箱或大陆手机号(服务端识别,无需 channel)。
|
||||
**Header `X-Language` 必填**(不接受仅 Accept-Language),用于邮件正文语言;未识别时默认 `en`。
|
||||
登录发码无需 Bearer;注销发码需 Bearer,且 `target` 须为当前绑定邮箱。
|
||||
邮箱经 SMTP(`noreply@sparkmail.wyrs365.com`);手机走阿里云短信。
|
||||
Body:`purpose` 1=登录 2=注销;`target` 为邮箱或大陆 11 位手机号(服务端识别,无需 channel)。
|
||||
**Header `X-Language` 必填**(不接受仅 Accept-Language),用于邮件主题/正文。
|
||||
登录发码无需 Bearer;注销发码需 Bearer,且 `target` 须为当前绑定邮箱(不支持手机号注销发码)。
|
||||
|
||||
**封控(失败均为 envelope `42902`,HTTP 仍 200)**
|
||||
|
||||
1. **按客户端 IP**(`RealIP`,依赖网关 `X-Real-IP` / PROXY):令牌桶,速率 `rate_limit.auth_per_second`(默认 5/s),与
|
||||
`POST /v1/auth/login`、`POST /v1/auth/token/refresh` **共用**同一 IP 桶。
|
||||
2. **按 target**:同一「渠道 + purpose + 规范化 target」**60 秒内**只能成功触发一次发信/发短信;Redis 键 `sms:cd:*`(无 Redis 时为进程内内存)。
|
||||
不同 purpose、不同邮箱/手机号互不影响。成功响应 `data.expires_in` 固定 **300**(码有效期秒数,与 60s 冷却无关)。
|
||||
|
||||
测试邮箱 `test01@spark.com`~`test10@spark.com` 不发信且不占用 target 冷却。详情 [docs/sms.md](../../../docs/sms.md)。
|
||||
|
||||
邮箱经 SMTP;手机走阿里云短信(按号段选运营商模板)。
|
||||
operationId: sendVerificationCode
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/X-Language"
|
||||
@ -691,7 +701,7 @@ paths:
|
||||
summary: 注销当前账号
|
||||
description: |
|
||||
吊销全部设备的 JWT 并踢掉 TCP。将 `users` 行迁入 `user_delete` 后删除原行(邮箱可重新注册)。
|
||||
非测试账号需先 `POST /v1/sms/codes`(purpose=2, channel=1)向绑定邮箱发码,再在本接口提交 `verify_code`。
|
||||
非测试账号需先 `POST /v1/sms/codes`(`purpose=2`,`target` 为绑定邮箱)发码,再在本接口提交 `verify_code`(遵守发码封控,见 docs/sms.md)。
|
||||
同时清除好友关系、朋友圈、收藏、群成员(群主把群转给一名管理员,没有管理员则转给成员表第一位后退出;仅剩自己则解散)、会话成员、收件箱 Redis 键与 mailbox。
|
||||
对方仍可见历史消息(不删 `messages`)。
|
||||
operationId: deleteMe
|
||||
|
||||
Loading…
Reference in New Issue
Block a user