gin 里没有“ip 防抖中间件”,只有按 ip 维度做令牌桶限流的中间件;不加维度隔离、不清理过期 key、不前置认证,就等于没设。

直接说结论:Gin 里没有“IP 防抖中间件”,只有按 IP 维度做令牌桶限流的中间件;不加维度隔离、不清理过期 key、不前置认证,就等于没设。
为什么用 rate.Limiter 却还是被刷穿
常见错误是全局共用一个 rate.Limiter 实例,比如 var limiter = rate.NewLimiter(rate.Limit(5), 10),然后所有请求都调它。这会导致:
- NAT 环境下几十人共享一个
c.ClientIP(),一人刷票,全组被锁 - 未登录用户和已登录用户混用同一桶,
c.GetString("user_id")为空时退化成纯 IP 限流,但你本意可能是“每个账号每分钟最多投 3 次” - 没做定时清理,
sync.Map里的 limiter 实例永远不释放,内存缓慢上涨
sync.Map 缓存 limiter 的正确姿势
必须为每个 IP 或 user_id 创建独立实例,并控制生命周期:
- key 用
c.ClientIP()或c.GetString("user_id"),不能拼错字符串 - value 是
*rate.Limiter,不是rate.Limiter(必须是指针,否则 copy 后状态不共享) - 启动一个 goroutine 定时扫描:
sync.Map.Range()检查 lastAccess 时间,超 5 分钟无访问就Delete() - 务必在认证中间件之后注册限流中间件,否则
c.GetString("user_id")取不到值
多实例部署时 Redis + Lua 是唯一可靠方案
负载均衡后,每个 Gin 实例只看到部分流量,内存版 rate.Limiter 完全失效。必须用 Redis 做原子计数:
- key 格式示例:
rate:ip:<code>c.ClientIP():vote/submit或rate:uid:<code>uid:vote/submit - Lua 脚本必须三步原子执行:读当前计数 → 判断是否超限 → 增加并设置过期(
EXPIRE),不能拆成多次GET+INCR - 连接池要配够:
MaxActive: 100,否则高并发下 Redis 连接耗尽比限流失效更早 - 别把粒度设太细,投票场景建议 “每分钟 10 次 + 突发允许 3 次”,而不是“每秒 1 次”
c.ClientIP() 可被伪造,必须提前信任代理头
默认 c.ClientIP() 取的是 RemoteAddr,在 Nginx + Gin 架构下会变成 127.0.0.1,且容易被伪造:
- 启动时必须调
r.SetTrustedProxies([]string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}) - 确保 Nginx 配置了
proxy_set_header X-Forwarded-For $remote_addr; - 如果前端走 CDN,还得把 CDN 的回源 IP 段加入
SetTrustedProxies
最常被忽略的点是:Lua 脚本里漏写 EXPIRE,导致 key 永不过期;还有 JWT 中设备指纹哈希没随每次登录更新,token 泄露后能长期复用——这两处一漏,五层防刷就塌了一半。











