发送短信验证码前必须校验图形验证码,否则防刷形同虚设;需用redis原子操作(如set sms_send_limit:{phone} 1 ex 60 nx)实现手机号级限频,并确保验证码存储带ttl且一次性使用。

发送前必须校验图形验证码
直接调用短信接口而不校验图形验证码,等于把防刷大门敞开。哪怕你后端做了限流,攻击者也能用脚本批量刷图码 + 短信组合请求,轻松绕过单手机号限频规则。
真实业务中,GetSmscd handler 里必须先取 uuid 和 text,再查 Redis 中对应 key 的图形码值(key 通常是 image_code_ + uuid)。不匹配就直接返回 400,不走后续逻辑。
- 图形码 key 过期时间建议设为 5 分钟,和短信验证码 TTL 一致,避免时序错乱
- 校验完立即
DEL image_code_{uuid},防止重复使用 - 注意:阿里云 SDK 不校验图形码,这是你自己的业务层职责,别指望第三方兜底
手机号级限频必须服务端强控制
前端加按钮禁用、加倒计时,全是障眼法。真正有效的限频只能靠服务端原子操作——用 Redis 的 INCR + EXPIRE 组合实现“60 秒内仅允许一次”。
典型写法是:SET sms_send_limit:{phone} 1 EX 60 NX。如果返回 1,说明首次请求,放行;返回 nil,说明已存在,拒绝发送。不能用 GET + INCR 两步,否则并发下会超发。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 别漏掉日志记录限频触发事件,方便后续分析攻击模式
- 同一手机号的短信验证码 key(如
sms_code:{phone})每次生成新码时,必须覆盖旧值,保证“最新有效”原则 - 注意 Redis 命令大小写敏感:
NX是大写,小写会失效
SDK 初始化时容易忽略 Domain 配置
用阿里云 Go SDK 时,dysmsapi.NewClientWithAccessKey 创建 client 后,如果不显式设置 request.Domain,默认会发往错误域名,报错 InvalidDomain.NotFound 或直接超时。
OpenAPI Explorer 生成的示例代码里有这行:request.Domain = "dysmsapi.aliyuncs.com",但很多开发者复制时删掉了,或者以为 region 参数能自动推导 domain —— 实际不能。
- region 决定 endpoint 路径,但不决定 host,
cn-hangzhou对应的 host 仍是dysmsapi.aliyuncs.com - 腾讯云 SDK 同理,
sms.NewClient初始化后,需手动设client.WithEndpoint("https://sms.tencentcloudapi.com") - 建议把 domain 封装进配置项,而不是硬编码在 send 函数里
验证码存 Redis 必须带过期且只读一次
存验证码只做 SET 不够,必须同时指定 TTL,且后续验证时用 GET + DEL 原子操作。否则会出现“验证成功后还能再用一次”的安全漏洞。
正确姿势是:GETDEL sms_code:{phone}(Redis 6.2+)或用 Lua 脚本封装 GET + DEL。Go 里可用 rdb.Get(ctx, key).Val() 后紧跟 rdb.Del(ctx, key),但要注意中间若服务崩溃,可能残留未删 key —— 所以 TTL 是最后一道保险。
- key 名建议统一前缀,比如
sms:code:,方便后期 scan 清理或监控 - 不要把验证码明文打到 error log 里,哪怕只是 debug 日志,
fmt.Printf("code: %s", code)是高危行为 - 6 位纯数字验证码,生成时用
rand.Intn(900000) + 100000比拼字符串更安全,避免前导零被截断










