短信验证码接入设计方案¶
背景¶
当前用户登录注册接口仍是 mock 逻辑:
POST /v1/users/send-code固定返回123456。POST /v1/users/login固定校验123456。
生产环境需要接入阿里云短信发送服务,并使用 Redis 保存验证码和有效期。为了控制短信成本、降低接口被刷风险,发送接口需要支持“有效期内复用”:同一手机号在验证码未过期时再次请求,不重新生成验证码,也不再次调用阿里云短信。
目标¶
- 接入阿里云短信服务,支持真实发送登录验证码。
- 验证码写入 Redis,默认 5 分钟有效。
- 5 分钟有效期内重复请求同一手机号时复用 Redis 中的验证码,不重发短信。
- 登录或注册时从 Redis 校验验证码。
- 验证成功后删除验证码,避免同一个验证码被重复使用。
- 保留本地开发 mock 模式,避免开发和测试环境依赖真实短信供应商。
- 配置、错误码、服务封装为后续注册、找回密码、绑定手机号等场景扩展留出空间。
非目标¶
- 第一阶段不做多短信供应商自动切换。
- 第一阶段不做图形验证码、滑块验证等人机校验。
- 第一阶段不引入数据库表保存验证码历史。
- 第一阶段不改造完整用户体系,只替换现有短信验证码发送与登录校验逻辑。
关键产品规则¶
验证码有效期¶
默认有效期为 5 分钟:
5 分钟是较常见的短信验证码有效期,能兼顾安全性和用户输入体验。短信偶发延迟、用户切换应用、复制粘贴验证码等场景通常可以覆盖。
有效期内复用¶
同一手机号、同一业务场景下,只要 Redis 中已有未过期验证码:
- 不重新生成验证码。
- 不调用阿里云短信接口。
- 直接返回成功,并告诉前端剩余有效期。
这意味着用户如果在 5 分钟内多次点击“发送验证码”,系统不会发多条短信,前端应提示用户查看上一条短信。
验证成功后删除¶
用户提交验证码并校验成功后,立即删除 Redis 中的验证码。这样即使 TTL 还没到,同一个验证码也不能再次使用。
阿里云发送失败不写 Redis¶
如果阿里云短信发送失败,不能把验证码写入 Redis。否则用户没有收到短信,却会被“5 分钟内不重发”的规则挡住。
主流程¶
发送验证码¶
flowchart TD
A["客户端请求发送验证码"] --> B["校验手机号格式"]
B --> C["读取 Redis: sms:code:login:{phone}"]
C --> D{"验证码是否存在且未过期"}
D -->|是| E["返回 success<br/>reused=true<br/>ttl_seconds=剩余秒数<br/>不调用阿里云"]
D -->|否| F["生成 6 位验证码"]
F --> G["调用阿里云 SendSms"]
G --> H{"发送是否成功"}
H -->|是| I["写入 Redis 并设置 TTL=300 秒"]
I --> J["返回 success<br/>reused=false<br/>ttl_seconds=300"]
H -->|否| K["返回短信发送失败<br/>不写 Redis"]
登录或自动注册¶
flowchart TD
A["客户端提交手机号和验证码"] --> B["校验手机号格式"]
B --> C["读取 Redis: sms:code:login:{phone}"]
C --> D{"验证码是否存在"}
D -->|否| E["返回验证码过期或未发送"]
D -->|是| F["对比用户输入验证码"]
F --> G{"是否一致"}
G -->|否| H["返回验证码错误"]
G -->|是| I["删除 Redis 验证码"]
I --> J["查找用户"]
J --> K{"用户是否存在"}
K -->|否| L["自动注册用户"]
K -->|是| M["检查用户状态"]
L --> N["签发 JWT"]
M --> N
Redis 设计¶
Key 命名¶
建议按业务场景区分:
当前登录注册一体场景:
后续可以扩展:
Value 结构¶
建议 Redis value 使用 JSON,而不是只存验证码字符串:
{
"code": "839204",
"phone": "13800138000",
"scene": "login",
"created_at": "2026-05-20T12:00:00+08:00"
}
原因:
- 方便排查问题。
- 方便后续增加发送渠道、模板编号、请求来源等字段。
- 避免未来扩展时改动 key 结构。
TTL¶
写入 Redis 时使用原子过期写入:
查询剩余有效期时使用:
并发注意事项¶
如果两个发送请求几乎同时到达,可能都读到 Redis 为空,然后都调用阿里云。为避免并发重复发送,建议使用短锁:
规则:
- 获取锁成功的请求负责生成验证码和发短信。
- 获取锁失败的请求短暂等待后重新读 Redis。
- 锁 TTL 建议 5 到 10 秒,避免发送进程异常时死锁。
第一阶段如果用户量较小,可以先不加锁;但生产环境建议加。
接口设计¶
发送验证码¶
沿用当前接口:
请求:
生产响应:
有效期内重复请求:
开发环境如开启调试返回验证码:
{
"code": 0,
"msg": "操作成功",
"data": {
"reused": false,
"ttl_seconds": 300,
"debug_code": "839204"
}
}
生产环境不能返回 debug_code。
验证码登录或注册¶
沿用当前接口:
请求:
响应仍保持现有结构:
服务分层¶
建议新增短信相关服务目录:
AliyunSmsService¶
职责:
- 读取阿里云短信配置。
- 调用阿里云
SendSms。 - 处理阿里云 SDK 异常。
- 将供应商错误转换成业务侧可理解的结果。
建议对外方法:
VerificationCodeService¶
职责:
- 手机号规范化和基础校验。
- 生成安全随机验证码。
- 查询 Redis 中是否已有未过期验证码。
- 控制 5 分钟内复用。
- 调用短信供应商发送。
- 写入 Redis 和设置 TTL。
- 登录时校验验证码。
- 校验成功后删除 Redis key。
建议对外方法:
async def send_login_code(phone: str) -> SendCodeResult:
...
async def verify_login_code(phone: str, code: str) -> None:
...
路由层只负责请求响应转换,不直接操作 Redis 或阿里云 SDK。
配置设计¶
建议新增配置项:
# 短信供应商:mock / aliyun
SMS_PROVIDER=mock
# 验证码规则
SMS_CODE_TTL_SECONDS=300
SMS_CODE_LENGTH=6
SMS_DEBUG_RETURN_CODE=false
# 阿里云短信
ALIYUN_SMS_ACCESS_KEY_ID=
ALIYUN_SMS_ACCESS_KEY_SECRET=
ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com
ALIYUN_SMS_SIGN_NAME=
ALIYUN_SMS_TEMPLATE_CODE=
说明:
- 本地开发默认
SMS_PROVIDER=mock,避免真实短信费用。 - 生产环境使用
SMS_PROVIDER=aliyun。 SMS_DEBUG_RETURN_CODE只能在开发环境打开。ALIYUN_SMS_TEMPLATE_CODE对应阿里云控制台审核通过的短信模板。- 模板变量建议统一使用
code,即阿里云模板参数为{"code":"839204"}。
错误码建议¶
当前已有:
建议新增:
也可以第一阶段只复用 INVALID_CODE 和 SYSTEM_ERROR,但从产品提示和排查角度,建议区分:
| 场景 | 建议错误码 | 用户提示 |
|---|---|---|
| 未发送或已过期 | SMS_CODE_EXPIRED |
验证码已过期,请重新获取 |
| 验证码输入错误 | INVALID_CODE |
验证码错误 |
| 阿里云发送失败 | SMS_SEND_FAILED |
短信发送失败,请稍后重试 |
| 生产配置缺失 | SMS_NOT_CONFIGURED |
短信服务未配置 |
| 防刷限制命中 | SMS_TOO_FREQUENT |
请求过于频繁,请稍后再试 |
安全与风控¶
第一阶段建议至少做到:
- 手机号格式校验,只接受合法手机号。
- 验证码使用安全随机数生成。
- 生产环境不返回验证码明文。
- 验证成功后删除验证码。
- 阿里云失败不写 Redis。
后续增强项:
- 按手机号限制每天发送次数。
- 按 IP 限制每分钟或每天发送次数。
- 验证码错误次数限制,超过阈值后要求重新获取。
- 管理后台或日志记录短信发送失败原因。
- 接入图形验证码或滑块,防止批量撞手机号。
建议 Redis key:
前端交互建议¶
发送成功后,前端开始 300 秒倒计时。
如果响应 reused=true:
- 不提示“已重新发送”。
- 提示“验证码仍在有效期内,请查看上一条短信”。
- 按
ttl_seconds继续倒计时。
如果响应 reused=false:
- 提示“验证码已发送”。
- 按
ttl_seconds开始倒计时。
实施步骤¶
阶段 1:服务封装与 mock 保留¶
- 新增短信配置项。
- 新增
VerificationCodeService。 - 先实现
SMS_PROVIDER=mock。 - 登录接口改为从 Redis 校验验证码。
- 保持本地开发可用。
阶段 2:阿里云短信接入¶
- 引入阿里云短信 SDK。
- 新增
AliyunSmsService。 - 支持生产环境
SMS_PROVIDER=aliyun。 - 处理阿里云发送失败和配置缺失。
- 更新
.env.example。
阶段 3:风控增强¶
- 增加手机号每日发送上限。
- 增加 IP 维度限流。
- 增加验证码错误次数限制。
- 必要时增加发送并发锁。
验收标准¶
- 第一次请求
send-code时,生成验证码并写入 Redis,TTL 为 300 秒。 - TTL 未过期时再次请求同一手机号,不调用短信发送服务,返回
reused=true。 - TTL 过期后再次请求,会生成新验证码并发送。
- 登录时验证码错误返回失败。
- 登录时验证码过期返回失败。
- 登录成功后 Redis 验证码被删除。
- 阿里云短信发送失败时 Redis 中没有可用验证码。
- 生产环境响应不包含验证码明文。
- mock 模式下本地开发可以不依赖阿里云完成登录流程。
待确认问题¶
- 生产环境是否坚持“5 分钟内完全不重发”,还是采用“验证码 5 分钟有效,60 秒后允许重发并刷新验证码”?
- 登录注册一体场景是否继续共用
loginscene? - 是否需要支持国际手机号,还是只支持中国大陆手机号?
- 阿里云短信模板变量名是否确定为
code? - 是否需要在管理侧记录短信发送日志,便于排查用户收不到短信的问题?