结论是防刷必须组合Token验证与频率限制,关键在限流粒度和Token绑定上下文;Token需绑定IP、操作类型等指纹信息,频率限制要分IP、用户、Token三级,且所有规则须可观察、可动态调整。

直接说结论:光靠 Token 验证防不了刷,光靠频率限制也拦不住高伪装请求;必须组合用,且关键在「限流粒度」和「Token 绑定上下文」两个地方。
Token 不是万能钥匙,它得带“指纹”才管用
很多人把 JWT 当成一次性门票,生成后就放行,结果被截获、重放、批量复用。真正的防刷 Token 必须绑定具体上下文:
- 必须绑定
user_ip或X-Forwarded-For(注意代理穿透问题) - 敏感操作 Token(如发短信)要绑定
action_type和session_id,不能拿登录 Token 去调用/send-sms - 服务端存 Redis 时,Key 要带签名前缀,比如
action_token:sha256(ip+uri+timestamp),防止 Key 碰撞或预测 - Token 本身有效期别设太长——
expires_in设 60 秒比 5 分钟更安全,尤其对验证码类接口
频率限制不能只按 IP,得看“谁在刷”
单纯用 limit:ip:1.2.3.4 容易误伤 NAT 用户,也防不住换 IP 的 bot。实际要分层打点:
- 第一层:IP + 接口路径,例如
rate:ip:/send-sms:1.2.3.4,每分钟最多 3 次 - 第二层:用户级(需登录),
rate:user:uid_123:/send-sms,每小时最多 5 次 - 第三层:Token 级,每个
action_token只能用一次,Redis 中设used:true并配EXPIRE双保险 - 注意滑动窗口实现:用 Redis 的
ZSET存时间戳,ZREMRANGEBYSCORE清旧数据,比简单INCR+EXPIRE更准
@RequestLimit 注解容易踩的三个坑
Spring Boot 里用自定义注解限流很常见,但线上出过不少问题:
-
key参数没做 SpEL 解析或硬编码为空,导致所有请求共用一个计数器,整个服务变“单线程” - 没考虑异步线程池场景,
ProceedingJoinPoint拿不到真实用户 ID,key里写#p0却传了String而非对象,SpEL 表达式直接报错吞掉异常 - Redis 连接超时没降级策略,缓存不可用时直接抛
RuntimeException,把限流变成“全站熔断” - 建议加一层兜底:当 Redis 不可用时,退化为内存级
ConcurrentHashMap计数(仅限单机部署)
防刷不是越严越好,关键在“可观察”
真正难的不是写限流逻辑,而是判断什么时候该限、限谁、限多狠。生产环境必须留出口:
- 所有限流拦截要打结构化日志,至少含
ip、uri、token_hash、limit_key、reason(如 “exceed_ip_rate”) - 提供实时监控端点,比如
GET /actuator/ratelimit-status,返回各限流规则当前 hit 数和 reject 数 - 不要静态配置阈值,像
maxCount=5这种写死参数,应该从配置中心动态加载,方便运营根据流量峰谷调整
最常被忽略的一点:限流规则本身也要防刷——如果攻击者发现你对 /health 不限流,他就能靠高频探针反推你的限流 Key 规则。所有管理类接口,同样要套 Token + 粒度限流。


















