必须用redis.setnx配合过期时间实现原子性防刷,因其能一次性判断存在性并设值,避免覆盖和竞态;key须带业务上下文与哈希手机号,校验需结合时间戳比对而非仅依赖redis过期机制。

为什么 redis.SetNX 是防刷关键操作
直接用 redis.Set 写验证码会导致覆盖已存在的记录,无法限制单位时间内请求次数;而 redis.SetNX(set if not exists)能原子性地判断“验证码是否已存在”,配合过期时间,才能真正卡住重复提交。它不是为了存数据,而是为了抢锁——谁先调用成功,谁获得本次发送资格。
常见错误是把 SetNX 和 Expire 拆成两步执行:先 SetNX 成功,再 Expire,中间若服务崩溃或网络中断,key 就变成永不过期的“僵尸验证码”。必须用单命令完成,例如:
redisClient.SetNX(ctx, "sms:phone:13800138000", "8848", 5*time.Minute)
注意:SetNX 的第三个参数是 time.Duration,不是秒数;传 300 会变成 300 纳秒,导致 key 瞬间过期。
怎么设计 Redis Key 避免手机号被绕过
只用 "sms:" + phone 太危险——攻击者可批量换号穷举,且不同业务(注册/登录/重置)共用一个频控 key,容易误伤。Key 必须携带业务上下文和来源标识:
-
"sms:register:ip:123.45.67.89"—— 按 IP 限流,适合前端直连场景 -
"sms:login:phone:13800138000"—— 按手机号+业务类型隔离,后端校验前必查 -
"sms:reset:token:abc123"—— 若走图形验证码前置流程,可用临时 token 绑定
别把敏感信息(如完整手机号)直接拼进 key 名做前缀——Redis key 过长会影响集群 slot 分配;建议统一用哈希(如 sha256(phone))截取前 12 位,既防猜解又控长度。
在 Echo 中拦截高频请求的中间件怎么写才不漏判
不能只靠 echo.MiddlewareFunc 包裹整个路由,因为短信发送接口往往需要解析 JSON body,而中间件默认拿不到原始 body。必须提前读取并缓存:
func RateLimitByPhone() echo.MiddlewareFunc {
return func(next echo.Handler) echo.Handler {
return func(c echo.Context) error {
// 提前解析 phone 字段,避免后续重复解析
var req struct{ Phone string `json:"phone"` }
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid phone")
}
key := "sms:register:phone:" + req.Phone
exists, err := redisClient.Exists(c.Request().Context(), key).Result()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "redis error")
}
if exists == 1 {
return echo.NewHTTPError(http.StatusTooManyRequests, "try again later")
}
// 允许通过,但要在 handler 中真正 SetNX,防止并发竞争
c.Set("sms_phone", req.Phone)
return next(c)
}
}
}
这个中间件只做“是否存在”的快速判断,真正的 SetNX 要放在业务 handler 里执行——否则并发请求可能都通过中间件,再一起写入 Redis,导致超发。
验证码校验时为什么不能只查 Redis 还要加时间戳比对
Redis key 过期是惰性删除 + 定期抽样,极端情况下 key 已逻辑过期但物理还存在。如果只靠 Get 返回非空就放行,可能校验到一个“僵尸验证码”。必须同时检查值内容结构:
推荐存储格式为 JSON:{"code":"123456","ts":1717023456},校验时:
- 用
redisClient.Get拿到字符串 - 反序列化后检查
ts是否在 5 分钟内(time.Now().Unix() - data.Ts ) - 再比对
code字段,顺序不能颠倒
别用 redisClient.TTL 查剩余时间——它返回 time.Duration,在 key 不存在时返回 -1,但刚过期未清理时可能返回 -2,语义模糊,不如自己算时间差可靠。
Redis 防刷真正难的不是命令怎么写,而是所有环节都要考虑并发、时钟漂移、网络分区和 key 设计的组合影响。比如 SetNX 成功但网络断开,客户端以为失败重试,结果发了两条;或者用本地时间生成验证码过期逻辑,容器时区没同步,导致校验永远失败。这些细节比代码多一行少一行更值得盯住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











