Laravel默认throttle中间件仅按IP限流且不支持动态策略,无法满足按用户ID、API Token或角色差异化限流需求;需自定义中间件,通过Redis::throttle()原子操作实现精准控制,并正确注册于路由中间件组中。

为什么 Laravel 的 throttle 中间件不直接满足你的需求
默认的 throttle 中间件基于 IP 限流,且绑定在路由组或控制器方法上,无法灵活适配「按用户 ID 限流」「按 API Token 限流」「区分免费/付费用户不同阈值」等场景。一旦你发现日志里出现 429 Too Many Requests 但实际请求来源混杂(比如多个用户共用一个出口 IP),就说明该中间件已失效。
自定义中间件不是为了造轮子,而是为了控制「识别主体」和「存储策略」这两个关键点。
如何写一个按 auth:user_id 限流的中间件
核心逻辑:从当前认证用户中提取 user_id,拼成 Redis key,用 INCR + EXPIRE 原子操作计数;超限时返回 429。
- 在
app/Http/Middleware/RateLimitByUser.php中创建类,handle方法内先检查auth()->check(),否则跳过或拒绝 - 使用
Cache::store('redis')->increment($key, 1)计数,再用Cache::store('redis')->add($key, 1, $seconds)设置过期(注意:add 只在 key 不存在时生效,需配合 increment 的原子性) - 推荐用
Redis::throttle($key)->allow(10)->every(60)->then(...)—— 这是 Laravel 封装好的原子限流器,比手写更可靠 - 不要在中间件里写
sleep()或复杂 DB 查询,避免阻塞请求生命周期
public function handle($request, Closure $next)
{
if (! auth()->check()) {
return response(['message' => 'Unauthorized'], 401);
}
<pre class="brush:php;toolbar:false;">$key = 'rate_limit:'.auth()->id();
$maxAttempts = 30;
$decayMinutes = 1;
$response = Redis::throttle($key)
->allow($maxAttempts)
->every($decayMinutes * 60)
->response(function () {
return response(['message' => 'Too many requests'], 429);
});
return $response->then(fn () => $next($request));}
配置中间件别名与注册时机很关键
中间件注册错位置会导致限流失效:比如注册在 web 中间件组里,但 API 请求走的是 api 组;或者注册在全局中间件里,却对登录接口也限流,造成死锁。
- 在
app/Http/Kernel.php的$routeMiddleware数组中添加:'throttle.by.user' => \App\Http\Middleware\RateLimitByUser::class - 只在需要限流的路由上显式调用:
Route::middleware(['auth:sanctum', 'throttle.by.user'])->group(...) - 切勿把该中间件加到
$middleware(全局数组)里——它依赖auth状态,而全局中间件执行时认证尚未完成 - 如果使用 Sanctum,确保
EnsureFrontendRequestsAreStateful已启用,否则auth()->check()在 API 请求中恒为 false
Redis 连接异常或键冲突时的表现与排查
线上最常遇到的不是逻辑错误,而是缓存层不可用导致限流完全失效(静默降级)或全量拦截(误判)。比如 Redis 连接超时后,Redis::throttle() 会抛出 Predis\Connection\ConnectionException,若没捕获,整个请求直接 500。
- 在
handle中用try/catch包裹 throttle 调用,捕获ConnectionException后 fallback 到宽松策略(如仅记录日志,不限制) - 键名必须唯一且可预期:避免用
$request->url()拼接,因为带查询参数的 URL 易被篡改;优先用auth()->id()或$request->bearerToken()的哈希值 - 本地开发用
array缓存驱动会绕过限流逻辑,务必在.env中设CACHE_DRIVER=redis并确认REDIS_HOST可达 - 用
redis-cli monitor观察实际生成的 key 和 TTL,比看代码更直观
实际跑通的关键不在“怎么写”,而在“谁来识别、在哪生效、挂了怎么办”。Redis 键设计不合理或中间件注册层级错了,比逻辑写错更难 debug。


















