revel框架无内置防刷中间件,需用redis原子操作+上下文提取+分层校验;filter无状态且不共享数据,session易被绕过;须用setnx+哈希前缀key+json value实现原子限流。

Revel 框架本身不提供开箱即用的防刷中间件,必须手动组合 Redis 原子操作 + 请求上下文提取 + 分层校验逻辑,否则单靠 Before 钩子或内存计数器完全无效。
为什么 Revel 的 Filter 不能直接防刷
Revel 的 Filter 是同步执行、无状态的,它不自动共享跨请求数据。你写个 var count int 在 filter 里自增,多实例部署下立刻失效;用 app.Session.Set 存频次?Session 默认基于 cookie,攻击者删 cookie 就重置——这等于没防。真正起作用的只能是外部存储 + 原子判断。
- 别在
Filter里用time.Now().Unix()简单比对上一次时间戳:Redis key 可能已过期但未被清理,查EXISTS返回 false,你以为空窗,其实只是惰性删除延迟 - 别用
GET + INCR两步操作:并发请求可能同时读到 nil,然后都写入 1,导致超发 - Revel 的
c.Params.Get("phone")取参前必须先做基础校验(如格式、非空),否则恶意传phone=或phone=123会绕过后续手机号维度限流
手机号维度必须用 SETNX + 过期时间原子写入
Key 必须带哈希前缀和业务标识,例如 sms:send:h: + sha256("13812345678"),避免明文手机号被扫描或泄露。Value 推荐存 JSON:{"ts":1728979200,"ip":"203.0.113.5"},方便后续关联分析。
- 错误写法:
redisClient.Set("sms:send:13812345678", "1", 3600)—— 覆盖旧值,失去“间隔控制”意义 - 正确写法:
redisClient.SetNX("sms:send:h:"+hash, value, 3600),Go 客户端返回bool表示是否首次设置成功 - 校验时不能只看 key 是否存在,必须
GET出 value 解析ts,再与当前时间比差值是否
IP 和设备指纹要组合滑动窗口限流
Revel 没有内置 IP 提取逻辑,c.Request.RemoteAddr 在 Nginx 后是内网地址,必须优先读 c.Request.Header.Get("X-Real-IP"),fallback 到 c.Request.Header.Get("X-Forwarded-For")(取第一个非私有 IP)。
- 单 IP 每小时最多 10 次:用
INCR sms:ip:203.0.113.5,再EXPIRE sms:ip:203.0.113.5 3600 NX初始化过期(注意:NX保证只设一次) - 设备指纹建议由前端生成并传
X-Device-ID,服务端拼进 key:sms:ipdev:203.0.113.5:abc123xyz,避免依赖 User-Agent 等易伪造字段 - 滑动窗口若用 ZSET,必须配合
ZREMRANGEBYSCORE key -inf (now-3600)清理,否则内存持续涨;但对短信这种低频操作,INCR+EXPIRE更轻量可靠
Revel 中间件里必须提前 return,不能调 c.Result = nil
Revel 的执行链不会因你返回而自动中断,c.Next() 不写就停,写了就继续往后走。防刷失败后必须立即响应并终止:
- 写法正确:
c.Response.Status = 429,c.RenderJSON(map[string]string{"msg": "too many requests"}),然后return - 绝对不要:
c.Result = revel.ERROR或c.RenderError(...)—— 这些不阻断流程,后面 handler 仍会执行 - 校验顺序建议硬编码在 filter 中:
IP/设备限流 → 手机号频控 → captchaToken 校验(如有)→ 最后才进 controller.SendSMS
最常被忽略的一点:Redis 操作失败(如连接超时、认证失败)必须当「放行」处理,而不是 panic 或 500。否则缓存故障时整个短信功能瘫痪,比被刷还致命。所有 redisClient 调用都要包 if err != nil { return },宁可漏防,不可误拦。











