topfans/docs/superpowers/specs/2026-05-22-sms-register-design.md
zheng020 aff114afdd docs: 补充阿里云短信服务详情
新增第12章:价格计费、模板类型、签名、开通流程、相关文档链接。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:43:44 +08:00

411 lines
14 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.

# 注册短信验证码功能设计文档
- **创建日期**: 2026-05-22
- **状态**: 待评审
- **目标**: 在用户注册流程中加入阿里云短信验证码验证
---
## 1. 背景与需求
当前注册流程:`手机号 + 密码 → 设置昵称 → 完成注册`
需求:在注册前增加短信验证码验证,确保手机号真实有效,防止恶意注册。
---
## 2. 交互流程设计
### 2.1 前端页面变化
修改 `frontend/pages/register/register.vue`
1. 用户输入手机号后,点击"发送验证码"按钮
2. 按钮变为倒计时状态60 秒),期间不可点击
3. 用户输入收到的验证码,点击"验证验证码"按钮
4. **验证通过后**:解锁密码输入框,按钮变为"注册"
5. 用户输入密码,点击"注册"按钮
6. 页面跳转到 `/pages/profile/setNickname`,携带手机号+密码+verify_token
注册流程图:
```
register.vue setNickname.vue
│ │
├── 输入手机号 │
├── 点击"发送验证码" ──→ 发送验证码 │
├── 输入验证码 │
├── 点击"验证验证码" ──→ 验证通过 │
│ 返回 verify_token │
├── 输入密码 │
├── 点击"注册" → 存储 temp_* │
│ + temp_register_verify_token │
└── 跳转 setNickname ─────────────→ │
├── 读取 temp_* + verify_token
├── 输入昵称 + 选择 star_id
└── 点击"下一步" ──→ register API
(mobile, password, nickname, star_id, verify_token)
```
**注意**setNickname.vue 调用 `store.dispatch('user/register', ...)` 时需传入 verify_token
前端 store 需透传 verify_token 到 registerApi。
### 2.2 验证码状态
| 状态 | 描述 |
|------|------|
| 未发送 | 默认状态,输入框禁用 |
| 已发送(倒计时中) | 输入框启用,倒计时按钮显示剩余秒数 |
| 已发送(可重发) | 倒计时结束,按钮恢复可点击 |
| 验证成功 | 输入框显示绿色对勾,继续注册流程 |
| 验证失败 | 显示错误提示,用户可重新发送 |
### 2.3 后端流程
```
发送验证码流程:
用户输入手机号 → 发送验证码请求 → 后端生成6位随机码 → 存入RedisHash结构60秒TTL → 调用阿里云SMS API → 返回前端
验证验证码流程:
用户输入验证码 → 验证请求 → 检查Redis中验证码是否存在 → 比对code字段 → 失败则递增attempts≥3次则删除Key → 验证成功则删除验证码Key并创建verify:register:{mobile}记录300秒TTL → 返回verify_token
注册流程需携带verify_token
前端提交注册信息 → 后端检查verify:register:{mobile}是否存在且匹配 → 不匹配则拒绝 → 匹配则处理注册(创建用户、粉丝档案) → 删除verify:register记录 → 返回注册成功
```
---
## 3. 实现方案
### 方案一:在 userService 中直接集成 SMS SDK推荐
```
前端 → 网关 /auth/send-code → userService → 阿里云SMS
前端 → 网关 /auth/verify-code → userService → Redis验证 → 继续注册流程
```
**优点:**
- 注册逻辑集中在一个服务,流程一目了然
- userService 已有完整注册流程,加 SMS 只是顺带的事
- Redis 已在 userService 中可用,不用跨服务调用
- 部署简单,只增加一个 SDK 依赖
**缺点:**
- userService 多了一个外部依赖(阿里云 SMS SDK升级 SDK 时需重新编译
- 如果未来短信需求变复杂userService 会越来越臃肿
**适用场景**:当前阶段最合适,注册流程本来就在 userService不需要为了"未来的扩展性"增加复杂度。
---
### 方案二SMS 单独作为一个微服务
```
前端 → 网关 /auth/send-code → 短信Service → 阿里云SMS
前端 → 网关 /auth/verify-code → userService → Redis验证
```
**优点:**
- 职责单一,短信服务可以独立迭代
- 未来如果加邮件、推送通知,继续扩展这个服务即可
- userService 保持干净,不感知第三方服务细节
**缺点:**
- 需要额外部署一个服务,增加运维复杂度
- 注册流程跨两个服务,一次注册调用两次 Dubbo延迟增加
- 目前项目没有独立的通知服务,新增一个微服务成本偏高
**适用场景**:适合未来有多渠道通知需求的大规模系统。
---
### 方案三:在 gateway 层调用 SMS
```
前端 → 网关 /auth/send-code → 阿里云SMS网关直接调用
前端 → 网关 /auth/verify-code → userService → Redis验证
```
**优点:**
- 阿里云 credentials 配置原本就在 gatewayOSS 用的是同一套),直接复用
- 前端请求在网关层就能触发短信,不用穿透到后端服务
- 网关是单点,处理短信很方便
**缺点:**
- 短信发送成功后的后续注册流程还在 userService逻辑割裂
- 网关承担了越来越多的职责路由、认证、OSS上传、现在又加短信越来越重
- 如果 SMS 认证配置要从 gateway 复用,需要确保 userService 能访问到同一份配置,增加了部署耦合
**适用场景**:适合短信作为独立操作、不属于用户注册主流程的场景。
---
## 4. 方案对比
| | 方案一userService | 方案二(独立微服务) | 方案三gateway |
|--|--|--|--|
| 部署复杂度 | 低 | 高 | 低 |
| 延迟 | 低 | 中 | 低 |
| 逻辑内聚性 | **高**(注册+短信在一起) | 高(短信独立) | 差(分散在两处) |
| 可扩展性 | 中 | **高** | 低 |
| 维护成本 | 低 | 中 | 低 |
---
## 5. 技术选型
| 项目 | 选择 |
|------|------|
| 短信 SDK | `github.com/aliyundysms/dysmsapi-go-sdk`(阿里官方 Go SDK需确认最新包名 |
| 认证方式 | AccessKeyID/Secret短信服务使用直接 AccessKey非 OSS 的 STS 方式) |
| 验证码存储 | Redis已部署过期自动失效 |
| 推荐方案 | **方案一**userService |
---
## 6. 配置项
`backend/services/userService/config/` 下新增 SMS 配置(采用方案一,配置跟随服务):
```go
// SMSConfig 短信配置
type SMSConfig struct {
AccessKeyID string
AccessKeySecret string
SignName string // 短信签名
TemplateCode string // 短信模板CODE
Region string // 区域(默认 cn-hangzhou
}
```
通过环境变量注入(与 gateway 的 OSS 配置分开,避免部署耦合):
```bash
# userService 环境变量
SMS_ACCESS_KEY_ID=
SMS_ACCESS_KEY_SECRET=
SMS_SIGN_NAME=TopFans
SMS_TEMPLATE_CODE=SMS_xxxxxxx
SMS_REGION=cn-hangzhou
```
---
## 7. Redis 存储设计
### 7.1 Key 结构(按场景区分)
| 场景 | Key 格式 | 说明 |
|------|----------|------|
| 注册 | `sms:register:{mobile}` | 注册时发送的验证码 |
| 登录 | `sms:login:{mobile}` | 登录时发送的验证码(未来扩展) |
| 修改密码 | `sms:password:{mobile}` | 忘记密码时发送的验证码(未来扩展) |
### 7.2 Value 结构Hash 类型,支持更多元数据)
```
Key: sms:register:13800138000
Value (Hash):
code: "123456" // 6位验证码
created_at: "1700000000" // 发送时间Unix timestamp
attempts: "0" // 验证失败次数超过3次需重新获取
used: "false" // 是否已使用
TTL: 60 秒
```
### 7.3 防暴力破解策略
- **失败计数**:每次验证失败递增 attempts 字段,≥ 3 次后删除 Key要求用户重新获取
- **发送频率限制**:每小时最多发送 10 次,超限返回 429
- 使用独立 Key `sms:limit:register:{mobile}` 计数String 类型TTL=3600 秒)
- **IP 维度限流**(可选):每 IP 每小时最多请求 30 次
- 使用独立 Key `sms:limit:ip:{ip}` 计数String 类型TTL=3600 秒)
### 7.4 验证后处理
验证成功后:
1. 删除验证码 Key防止重放`DEL sms:register:{mobile}`
2. 创建验证通过记录:`verify:register:{mobile}` = `{token}`TTL = 300 秒
3. 注册接口使用 verify_token 比对,一致则处理注册,注册成功后删除该记录
---
## 8. API 设计
### 8.1 发送验证码
**请求**
```
POST /api/v1/auth/send-code
Content-Type: application/json
{
"mobile": "13800138000"
}
```
**响应**
```json
{
"code": 200,
"message": "发送成功",
"data": {
"expires_in": 60
}
}
```
### 8.2 验证验证码
**请求**
```
POST /api/v1/auth/verify-code
Content-Type: application/json
{
"mobile": "13800138000",
"code": "123456"
}
```
**响应**
```json
{
"code": 200,
"message": "验证成功",
"data": {
"verified": true,
"verify_token": "vtf_abc123xyz789...",
"expires_in": 300
}
}
```
**说明**
- `verify_token` 是长度为 32 位的随机字符串(格式:`vtf_` + 29 位随机字符),不是 JWT
- 存储在 Redis 中:`verify:register:{mobile}` → `vtf_abc123xyz789...`TTL = 300 秒
- 注册接口需携带此 token验证通过后才处理注册请求
- 验证成功后删除该记录,防止重复使用
**注册接口携带 token**
```
POST /api/v1/auth/register
Content-Type: application/json
{
"mobile": "13800138000",
"verify_token": "vtf_abc123xyz789...",
"password": "xxx",
"nickname": "xxx",
"star_id": 1
}
```
后端逻辑:
1. 根据 mobile 从 Redis 中获取 `verify:register:{mobile}` 的值
2. 与请求中的 verify_token 比对,不一致则拒绝
3. 验证通过后删除该记录,防止重复使用
---
## 9. 错误处理
| 场景 | 错误码 | 处理方式 |
|------|--------|----------|
| 手机号格式错误 | 400 | 提示"手机号格式不正确" |
| 短信发送失败(阿里云超时) | 500 | 提示"服务暂不可用,请稍后重试" |
| 短信发送失败(配额不足) | 500 | 提示"发送失败,请联系客服" |
| Redis 连接失败 | 500 | 提示"验证服务暂不可用" |
| 验证码已过期 | 400 | 提示"验证码已过期,请重新获取" |
| 验证码错误 | 400 | 提示"验证码错误剩余N次" |
| 验证码错误超过3次 | 400 | 提示"验证失败次数过多,请重新获取" |
| 验证码已使用 | 400 | 提示"验证码已使用,请重新获取" |
| 发送频率超限60秒内重复发送 | 429 | 提示"发送过于频繁,请稍后再试" |
| 每小时发送次数超限(>10次 | 429 | 提示"当前手机号发送次数超限,请稍后再试" |
| IP 请求频率超限 | 429 | 提示"请求过于频繁,请稍后再试" |
---
## 10. 部署说明
1. 在阿里云短信服务控制台创建签名和模板,获取 `SignName``TemplateCode`
2.`deploy/envs/user.env` 中配置阿里云短信 AccessKey
```bash
# 阿里云短信配置(直接使用 AccessKey非 STS
SMS_ACCESS_KEY_ID=your_access_key_id
SMS_ACCESS_KEY_SECRET=your_access_key_secret
SMS_SIGN_NAME=TopFans
SMS_TEMPLATE_CODE=SMS_xxxxxxx
SMS_REGION=cn-hangzhou
```
3. 重启 userService 服务
---
## 11. 安全注意事项
1. **验证码日志脱敏**:日志中禁止记录明文验证码,只能记录手机号和发送状态
2. **verify_token 安全**token 有效期 5 分钟,只能使用一次,验证后立即删除
3. **防暴力破解**:验证失败 3 次后强制删除验证码,要求用户重新获取
4. **限流保护**:从手机号和 IP 两个维度限制请求频率
---
## 12. 阿里云短信服务详情
### 12.1 价格计费
| 计费项 | 说明 |
|--------|------|
| 国内短信 | 0.045 元/条(正常模板) |
| 国际/港澳台短信 | 按区域定价,详见 [阿里云短信定价页](https://www.aliyun.com/product/sms) |
> ⚠️ **待确认**:请在实际使用前登录阿里云控制台确认最新价格,本文档价格仅供参考。
### 12.2 短信模板类型
| 模板类型 | 说明 | 审核时间 |
|----------|------|----------|
| 验证码模板 | 仅含变量(如 ${code}),用于发送随机验证码 | 通常 2 小时内 |
| 通知模板 | 含固定文本 + 少量变量 | 通常 2 小时内 |
| 推广模板 | 营销类内容,审核更严格 | 通常 4 小时以上 |
**验证码模板示例:**
```
您的注册验证码是${code}5分钟内有效。如非本人操作请忽略此短信。
```
### 12.3 签名
- 签名是短信发送者的标识,位于短信内容开头
- 一个账号可创建多个签名,需审核通过后才能使用
- 签名名称示例:`TopFans`、`TOPFANS官方`
### 12.4 开通流程
1. 登录阿里云控制台 → 产品 → 短信服务 SMS
2. 完成企业认证(如尚未认证)
3. 创建短信签名:控制台 → 短信签名管理 → 新增签名
4. 创建短信模板:控制台 → 短信模板管理 → 新增模板
5. 获取 AccessKeyRAM 控制台 → 访问密钥 → 创建 AccessKey
6. 记录以下信息填入配置:
- AccessKey ID
- AccessKey Secret
- 签名名称SignName
- 模板 CODETemplateCode
### 12.5 相关文档
- [阿里云短信服务帮助文档](https://help.aliyun.com/zh/sms)
- [短信 API 调用指南](https://help.aliyun.com/zh/sms/developer-reference/sendSms)
- [Go SDK 文档](https://help.aliyun.com/zh/sms/developer-reference/aliyun-java-sdk-sms)
---
## 13. 待确定事项
- [ ] 阿里云短信签名和模板CODE需在阿里云控制台创建
- [ ] 验证码有效期(默认 60 秒是否合适)
- [ ] 每小时同一手机号发送次数上限(默认 10 次)
- [ ] 验证失败次数上限(默认 3 次后强制重新获取)