应使用crypto/rand.Read生成6位真随机数字字符串:读取6字节,对10取模映射为'0'-'9',转字符串确保前导零不丢失;Redis存储必须设5分钟TTL且用GETDEL原子校验,同时对手机号做正则校验、滑动窗口频控和黑名单过滤。

短信验证码怎么生成才安全又实用
直接用 rand.Intn(10) 拼 6 位数是常见错误,它不满足密码学安全要求,容易被预测。Go 标准库的 crypto/rand 才适合生成真正随机的验证码。
- 用
crypto/rand.Read()读取字节,再映射到数字字符(0–9),避免使用math/rand - 不要直接返回
int类型的验证码值,始终以字符串形式存储和比对(防止前导零丢失) - 验证码长度建议固定为 6 位,太短易暴力破解,太长影响用户体验
func generateCode() string {
b := make([]byte, 6)
_, _ = rand.Read(b)
for i := range b {
b[i] = '0' + b[i]%10
}
return string(b)
}Redis 存验证码为什么必须带过期时间
不设 TTL 是最常踩的坑:用户反复请求导致 Redis 键无限堆积,内存爆满;更严重的是,旧验证码未自动失效,攻击者可能重放使用。
- 写入时必须用
SET key value EX 300(5 分钟),不要先SET再EXPIRE,这两步非原子操作,中间若服务崩溃会导致永不过期 - Go 客户端推荐用
redis.Client.Set(ctx, key, value, 5*time.Minute),底层自动拼 EX 参数 - key 命名建议含业务前缀和手机号,例如
sms:login:13800138000,避免键冲突
校验时怎么避免“时间差攻击”和并发覆盖
用户点击登录按钮多次,或网络延迟导致多个请求几乎同时到达,容易出现“验证码已用过但还没删,又被另一个请求成功校验”。
- 校验逻辑必须原子:用
GETDEL(Redis 6.2+)或EVALLua 脚本,一次性读取并删除 - Go 中用
redis.Client.GetDel(ctx, key)最简洁;若 Redis 版本低,写 Lua:if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end - 不要先
Get再Del:中间窗口期可能被重复提交
手机号格式与发送限频怎么防刷
只校验是否为 11 位数字远远不够。真实场景中,需在发码前做三层过滤:
立即学习“go语言免费学习笔记(深入)”;
- 基础格式:用正则
^1[3-9]\d{9}$初筛,排除明显非法号 - 频控:对
phone+action=login组合,用 Redis 的INCR + EXPIRE实现滑动窗口,例如“1 小时内最多 5 次” - 黑名单:维护一个
blacklist:phoneSet,对高频异常号码临时拉黑 24 小时
真正难处理的是“同一设备反复换号轰炸”,这需要结合前端行为埋点(如页面停留时长、点击间隔)或接入风控 SDK,纯后端无法根治。
Redis 的过期策略不是实时触发,而是惰性+定期删除混合;所以不能依赖“过期即消失”来做业务判断——所有校验都必须显式检查返回值是否为空。


















