单纯靠前端防抖拦不住刷票,因为防抖仅在客户端延迟发送请求,服务端仍会收到每个独立http请求;后端需结合限流、redis原子计数、referer校验、jwt设备绑定及区块哈希链校验五层防护。

为什么单纯靠前端防抖拦不住刷票
防抖(debounce)是前端 JS 用 setTimeout 延迟执行的逻辑,服务端根本收不到“用户还没点下一次”的状态。所有 HTTP 请求都是独立到达 Gin 的,你没法让 c.Next() 等 300ms 再走,更不能合并请求。所谓“后端防抖”,本质就是限流 + 拒绝策略。
用 golang.org/x/time/rate 做单实例 IP 限流的坑
直接套用 rate.NewLimiter(rate.Every(time.Second/5), 10) 全局限流会误伤——NAT 下几十人共用一个 c.ClientIP(),一人刷崩全组。必须按维度隔离:
- 每个 IP 或登录态
user_id对应独立的*rate.Limiter实例,不能共用一个桶 - 用
sync.Map缓存 limiter,key 是c.ClientIP()或c.GetString("user_id") - 必须加定时清理:5 分钟无访问就
delete对应 entry,否则内存泄漏 -
c.ClientIP()可被伪造,得提前调router.SetTrustedProxies([]string{"10.0.0.0/8"})并信任X-Forwarded-For - 认证中间件必须在限流中间件之前注册,否则
c.GetString("user_id")为空,退化成 IP 限流
多实例部署时 Redis + Lua 才是真防刷
负载均衡下,每个 Gin 实例只管自己收到的请求,rate.Limiter 完全失效。必须用 Redis 做原子计数:
- Key 设计示例:
rate:ip:<code>c.ClientIP():vote/submit或rate:uid:<code>uid:vote/submit - 用 Lua 脚本保证“读+增+过期”三步原子性,避免竞态
- 注意 Redis 连接池配置,高并发下连接耗尽比限流失效更致命
- 别把限流粒度设太细(比如每秒 1 次),投票场景需容忍合法连点,建议按分钟级+突发阈值组合
Referer 白名单和 JWT 认证不是可选项
仅靠限流挡不住自动化脚本,必须叠加校验层:
- 投票接口强制校验
Referer,白名单只放你自己的前端域名,代码里用url.Parse(referer).Host提取比对 - JWT 必须在限流前验证,且 payload 中带
user_id和设备指纹哈希(如 UA + IP hash),防止 token 泄露后被复用 - 实名投票接口额外校验区块链存证链的上一区块哈希,确保操作不可篡改——这个链式校验容易被跳过











