Laravel如何限制接口请求频率?(防刷限流)

1次阅读

laravel 使用 ratelimiter 实现限流需配置 redis 缓存、自定义 key(如设备指纹)、避免 key 冲突,并针对高频接口采用滑动窗口或条件绕过,同时加强监控与调试。

Laravel如何限制接口请求频率?(防刷限流)

RateLimiter 配置全局或路由级限流规则

Laravel 内置的 RateLimiter 是最直接、也最可控的方式。它不依赖外部服务(如 Redis),但默认走缓存驱动,实际生产中建议配 Redis 保证一致性。

常见错误是只改 app/http/Kernel.php 里的中间件参数,却没确认缓存驱动是否生效——比如用 Array 缓存时,多进程下限流完全失效。

  • app/Http/Kernel.php$middlewareGroups['api'] 中确保已包含 IlluminateRoutingMiddlewareThrottleRequests::class
  • 路由定义时可显式传参:->middleware('throttle:60,1') 表示每分钟最多 60 次请求
  • 若需区分用户,加 throttle:60,1,by=auth:id;匿名用户则默认按 IP(by=request:ip
  • 注意:Laravel 9+ 默认使用 cache 存储计数器,检查 CACHE_DRIVER 是否为 redismemcached

自定义限流键(key)避免误杀或漏限

默认按 IP 或登录用户 ID 限流太粗,比如 API 走 CDN 后所有请求共享一个 IP,或者多个前端共用同一账号时互相干扰。这时候必须重写限流键逻辑。

典型场景:App 端带设备指纹、小程序带 openid、后台接口需按 tenant_id 隔离。不改 key 就等于没限流。

  • app/Providers/AppServiceProvider.phpboot() 中调用 RateLimiter::for() 注册新策略
  • 例如按请求头中的 X-Device-ID 限流:RateLimiter::for('api-by-device', function (Request $request) { return Limit::perMinute(30)->by($request->header('X-Device-ID')); });
  • 别忘了在路由里引用:->middleware('throttle:api-by-device')
  • 注意:by() 返回值必须是字符串,且不能含特殊字符(如空格、斜杠),否则缓存 key 会出错

ThrottleRequests 中间件报 TooManyRequestsHttpException 怎么调试

这个异常本身不可怕,但线上突然大量出现,往往说明限流策略和实际流量不匹配,或是 key 冲突导致“一人触发、全员受限”。

常见诱因不是阈值设低了,而是 key 太宽泛——比如用了 by=auth:id,但 Token 过期后用户反复重登,旧 session 还在计数器里占坑。

  • 先看日志里 TooManyRequestsHttpException 出现前的请求路径和 header,确认是否集中在某个 endpoint 或某类客户端
  • 临时加日志到 ThrottleRequests::handleRequest()(不建议直接改 vendor,可用事件监听或装饰中间件)
  • php artisan tinker 手动查缓存:Cache::get('throttle|123.45.67.89')(key 格式取决于你用的 by 规则)
  • Redis 中 key 名类似 throttle|api|user:123,TTL 应与限流窗口一致;若 TTL 异常长,可能是缓存驱动未正确设置过期

高频接口(如轮询、心跳)怎么绕过或降级限流

不是所有接口都适合一刀切限流。比如设备心跳每 10 秒一次,按默认每分钟 60 次就刚好卡死;又或者搜索建议接口被用户狂敲键盘触发,需要更细粒度控制。

硬性绕过限流风险大,但可以换策略:抽样统计、动态阈值、或前置过滤。

  • 对轮询类接口,改用滑动窗口:throttle:300,60,by=request:ip → 改成 throttle:300,60,by=concat(request:ip,request:header.User-Agent) 分散压力
  • throttle:0 完全关闭限流(仅测试环境),生产中应配合前置条件判断,比如只对非 GET /health 关闭
  • 更稳妥的做法是提取公共逻辑到中间件,在 handle 前判断 $request->is('api/v1/heartbeat'),满足则跳过 parent::handleRequest()
  • 注意:任何“绕过”都要留监控出口,比如记录 bypass 次数到 StatsD 或 Log,否则限流形同虚设

真正难的不是配几个数字,而是搞清谁在调、为什么调、调完谁来兜底。比如同一个限流 key 在 CLI 命令和 HTTP 请求里混用,或者队列任务里误触了 ThrottleRequests,这些地方不会报错,但会让限流失效得悄无声息。

text=ZqhQzanResources