倒计时逻辑必须由后端控制,前端仅作展示:用户点击后先调用后端接口,后端通过 redis 检查手机号是否已存在未过期的 sms:verify:{hash} key,存在则返回 429 及剩余 ttl,不存在则写入并设 ttl 为 60 秒;前端据此启动或提示倒计时。

倒计时逻辑不该放在前端做
用户输入手机号点击“获取验证码”后,前端显示 60 秒倒计时,但仅靠 setInterval 和前端时间控制是危险的:用户可篡改浏览器时间、刷新页面重置、或禁用 JS 绕过限制。真实有效的倒计时必须由后端控制发放节奏,并配合 Redis 缓存校验。
Gin 中典型做法是:用户请求 /api/send-sms → 后端检查 Redis 中该手机号是否已有未过期的 sms:verify:{phone} key → 若存在,返回错误(如 {"code":429,"msg":"请60秒后再试"})→ 若不存在,写入 key 并设 TTL 为 60 秒,同时触发短信发送。
- Redis key 命名建议带业务前缀和手机号哈希(防扫描),例如
sms:verify:sha256(138****1234) - TTL 必须严格设为 60 秒,不能依赖前端传来的“剩余秒数”
- 短信发送失败时,仍要写入 key(防止重复轰炸),但需记录失败日志供排查
Gin 路由里怎么校验 Redis 状态
别在每个 handler 里手写 redis.Client.Get() + ErrNil 判断。封装成中间件或工具函数更可靠:
func CanSendSMS(ctx *gin.Context, rdb *redis.Client, phone string) (bool, error) {
key := "sms:verify:" + sha256String(phone)
val, err := rdb.Get(ctx, key).Result()
if err == redis.Nil {
// 未命中,允许发送
err = rdb.Set(ctx, key, "1", 60*time.Second).Err()
return err == nil, err
}
if err != nil {
return false, err
}
return false, nil // 已存在,拒绝发送
}
调用时注意传入 context.WithTimeout 防止 Redis 慢查询拖垮整个接口;sha256String 是简单哈希,避免手机号明文出现在 key 中(防批量探测)。
- 不要用
time.Now().Unix()做 key 过期判断——Redis 自带 TTL,无需手动维护时间戳 - 如果用的是 Redis Cluster,确保 key 的 hash tag(花括号内)保证路由一致性,比如
sms:verify:{138****1234} - 测试时用
redis-cli set sms:verify:test 1 EX 60手动注入数据验证逻辑
前端倒计时怎么和后端状态对齐
前端点击“获取验证码”按钮后,应先发一次请求到 /api/send-sms,成功才启动本地倒计时;失败则根据响应码(如 429)提示用户“还有 XX 秒可重试”。关键点在于:后端要在 429 响应中附带剩余 TTL。
修改上面的 CanSendSMS 函数,在命中缓存时读取剩余 TTL:
if err == redis.Nil {
// ... 正常设置
} else if err != nil {
return false, err
} else {
ttl, _ := rdb.TTL(ctx, key).Result() // 返回 time.Duration
remaining := int(ttl.Seconds())
if remaining <p>这样前端拿到 <code>remaining</code> 字段,就能准确显示“58秒后重试”,而不是硬写死 60 秒。</p>
- 注意
rdb.TTL()返回负值表示 key 不存在或已过期,此时应走正常发送流程 - 前端倒计时用
setTimeout而非setInterval更稳妥,避免因页面失焦导致计时偏差 - 按钮禁用状态必须同时受后端响应和前端倒计时双重控制
为什么不用 Gin 内置 session 做限频
因为 Gin 的 gin-contrib/sessions 默认基于内存或 Cookie,不支持跨实例共享,而短信接口通常部署多副本。若用内存 session,用户可能刚被 A 实例拒绝,又从 B 实例成功发送——直接导致限频失效。
Redis 是唯一靠谱的选择:所有实例共用同一份缓存,且支持原子性操作(如 SET key value EX 60 NX)。别图省事用 sync.Map 或文件存储,线上环境必然出问题。
- 哪怕只部署单实例,也建议用 Redis——方便后续横向扩展
- 不要把验证码本身(如 “123456”)和倒计时 key 存一起;验证码应单独用另一个 key(如
sms:code:{hash}),并设更短 TTL(如 5 分钟) - 生产环境务必给 Redis 连接加连接池和超时(
redis.Options{PoolSize: 20, DialTimeout: 5 * time.Second})
实际最难的部分不是写倒计时,而是让前后端在“用户感知的倒计时”和“服务端真实的冷却窗口”之间保持严格一致——差 1 秒都可能被恶意利用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











