限流必须放在中间件且位于权限校验之前,仅控制器计数无效;tp6的ratelimit需显式挂载路由,key设计要防误共享,redis计数须用lua原子操作,按用户限流需重写getlimitedid()。

限流必须放在中间件里,控制器里写 incr 或 cache 计数等于没做——它既无法覆盖全路由,又在权限校验前就失效,更扛不住并发。
为什么RateLimit中间件默认不生效
TP6 自带的 think\middleware\RateLimit 类只是注册了,没挂载到任何路由。压测时完全没反应,不是配置错,是根本没跑起来。
- 必须在路由定义中显式调用
->middleware('throttle:60,1'),光有类和配置没用 - 分组挂载只对子路由生效,
Route::group('api', ...)不会自动包含/api这个路径本身 - 参数
60,1表示 60 次 / 1 分钟,第二个数字单位是「分钟」,支持小数(如0.5= 30 秒),但别设太小(0.01会导致 Redis key 刷新过频) - 若用 Redis 存储,确认
config/cache.php中default已设为'redis',否则计数写进文件缓存,多进程下直接失效
Throttle中间件 key 参数怎么填才不翻车
key 决定谁和谁共用一个计数器。填错会导致“一人被封全网陪绑”或“恶意用户换接口就绕过”。
-
'__IP__'最常用,但注意:Nginx/CDN 后的$request->ip()可能是逗号分隔串(如"1.1.1.1,2.2.2.2"),得先取第一个公网 IP 并清洗 -
'__CONTROLLER__/__ACTION__'适合 RESTful 接口,但闭包路由或资源路由里可能解析为空,导致所有请求挤进同一个 key - 上传接口慎用
true(等价于仅 IP),应自定义函数区分场景:function($throttle, $request) { return $request->ip() . '_' . ($request->post('scene') ?: 'default'); } - 别把敏感信息(如 token、手机号)直接拼进 key,Redis key 是明文可见的;必要时用
md5()哈希,但注意哈希碰撞风险
高并发下 Redis 计数为什么还会超?
用 $redis->incr($key); $redis->expire($key, 60) 两步走,在每秒上百请求时必然出现“计数+1 成功,但 expire 没执行”的窗口期——这就是穿透根源。
- ThinkPHP 的
think-throttlev2+ 默认已改用 Lua 脚本,但如果你手写中间件,必须自己封装原子操作 - 正确脚本模板:
local current = redis.call("incr", KEYS[1]) if current == 1 then redis.call("expire", KEYS[1], ARGV[1]) end return current - 调用时传
[$key]和[60],别拼字符串,否则 Redis Cluster 下 key 落点不一致 - 首次
INCR返回 1 才设过期,避免反复EXPIRE干扰 TTL 统计
按登录用户限流要重写 getLimitedId()
默认按 IP,但后台导出、支付回调这类接口,真正要控的是「每个用户每天最多调用 N 次」,和 IP 无关。
- 新建类
app\http\middleware\UserRateLimit,继承think\middleware\RateLimit - 重写
getLimitedId(),返回$request->user()?->id ?? $request->ip(),确保未登录 fallback 到 IP - 在
app/middleware.php中注册别名,如'user_throttle' => \app\http\middleware\UserRateLimit::class - 路由中改用
->middleware('user_throttle:100,1'),其余逻辑自动复用
最易被忽略的一点:限流中间件必须在权限校验中间件之前注册,否则未登录用户可能绕过限流直接打爆登录接口;而 ValidateToken 这类 CSRF 校验中间件,又必须在限流之后——顺序错了,一个中间件就能让整个防护链失效。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











