# 注册短信验证码功能设计文档 - **创建日期**: 2026-05-22 - **状态**: 待评审 - **目标**: 在用户注册流程中加入阿里云短信验证码验证 --- ## 1. 背景与需求 当前注册流程:`手机号 + 密码 → 设置昵称 → 完成注册` 需求:在注册前增加短信验证码验证,确保手机号真实有效,防止恶意注册。 --- ## 2. 交互流程设计 ### 2.1 前端页面变化 修改 `frontend/pages/register/register.vue`,在当前页面嵌入短信验证码输入区: 1. 用户输入手机号后,点击"发送验证码"按钮 2. 按钮变为倒计时状态(60 秒),期间不可点击 3. 用户输入收到的验证码 4. 点击"验证验证码"按钮 5. 验证通过后解锁密码输入框,继续原有注册流程 ### 2.2 验证码状态 | 状态 | 描述 | |------|------| | 未发送 | 默认状态,输入框禁用 | | 已发送(倒计时中) | 输入框启用,倒计时按钮显示剩余秒数 | | 已发送(可重发) | 倒计时结束,按钮恢复可点击 | | 验证成功 | 输入框显示绿色对勾,继续注册流程 | | 验证失败 | 显示错误提示,用户可重新发送 | ### 2.3 后端流程 ``` 用户输入手机号 → 发送验证码请求 → 后端生成6位随机码 → 存入Redis(60秒有效期) → 调用阿里云SMS API → 返回前端 用户输入验证码 → 验证请求 → 后端从Redis取出比对 → 一致则标记验证成功 → 继续注册流程 ``` --- ## 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 配置原本就在 gateway(OSS 用的是同一套),直接复用 - 前端请求在网关层就能触发短信,不用穿透到后端服务 - 网关是单点,处理短信很方便 **缺点:** - 短信发送成功后的后续注册流程还在 userService,逻辑割裂 - 网关承担了越来越多的职责(路由、认证、OSS上传、现在又加短信),越来越重 - 如果 SMS 认证配置要从 gateway 复用,需要确保 userService 能访问到同一份配置,增加了部署耦合 **适用场景**:适合短信作为独立操作、不属于用户注册主流程的场景。 --- ## 4. 方案对比 | | 方案一(userService) | 方案二(独立微服务) | 方案三(gateway) | |--|--|--|--| | 部署复杂度 | 低 | 高 | 低 | | 延迟 | 低 | 中 | 低 | | 逻辑内聚性 | **高**(注册+短信在一起) | 高(短信独立) | 差(分散在两处) | | 可扩展性 | 中 | **高** | 低 | | 维护成本 | 低 | 中 | 低 | --- ## 5. 技术选型 | 项目 | 选择 | |------|------| | 短信 SDK | `github.com/aliyundysms/dysmsapi-go-sdk`(阿里官方 Go SDK) | | 认证方式 | AccessKeyID/Secret(与 OSS 配置复用同一套) | | 验证码存储 | 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,要求用户重新获取 - **发送频率限制**:记录 `sms:limit:{mobile}:hour`,每小时最多发送 10 次,超限返回 429 - **IP 维度限流**(可选):记录 `sms:limit:{ip}:hour`,每 IP 每小时最多请求 30 次 ### 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 # 阿里云短信配置 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 # 阿里云 OSS 配置(复用同一套凭证) OSS_ACCESS_KEY_ID=your_access_key_id OSS_ACCESS_KEY_SECRET=your_access_key_secret OSS_STS_ROLE_ARN=acs:ram::1387642798143585:role/top-fans-oss-user ``` 3. 重启 userService 服务 --- ## 11. 安全注意事项 1. **验证码日志脱敏**:日志中禁止记录明文验证码,只能记录手机号和发送状态 2. **verify_token 安全**:token 有效期 5 分钟,只能使用一次,验证后立即删除 3. **防暴力破解**:验证失败 3 次后强制删除验证码,要求用户重新获取 4. **限流保护**:从手机号和 IP 两个维度限制请求频率 --- ## 12. 待确定事项 - [ ] 阿里云短信签名和模板CODE(需在阿里云控制台创建) - [ ] 验证码有效期(默认 60 秒是否合适) - [ ] 每小时同一手机号发送次数上限(默认 10 次) - [ ] 验证失败次数上限(默认 3 次后强制重新获取)