Yii2需自定义ActionFilter行为实现IP限流:在beforeAction中用Redis原子操作计数,按路由/角色动态匹配规则,设置429响应及X-RateLimit头,Redis故障时fail-open。

Yii2里怎么用自定义行为做IP限流
直接说结论:Yii2没有开箱即用的API限流组件,RateLimiter 类只在 console 应用里默认可用,Web 请求必须自己挂载过滤逻辑。最稳妥的做法是写一个继承 ActionFilter 的自定义行为,在 beforeAction 里拦截并计数。
常见错误是直接在控制器里写限流逻辑——这会导致复用困难、测试难覆盖、且无法统一响应格式(比如该返回 429 还是 JSON 错误?)。另一个坑是把计数器存在 PHP 内存或 Session 里,重启就清零,完全失效。
- 用
yii\caching\Cache(如 Redis)存 IP + 时间窗口计数,Key 格式建议为"rate_limit:ip:{ip}:{window_start}" - 时间窗口按秒对齐(比如每分钟限 10 次,就用
floor(time() / 60)生成窗口标识),避免跨窗口漏判 - 务必在
beforeAction返回false中断请求,并手动设置Response::setStatusCode(429)和Content-Type: application/json
Redis 计数器怎么防并发超限
多个请求几乎同时抵达时,靠 PHP 层 if-else 判断再 incr 很容易超限——因为两次读取之间可能都还没写入,结果都放行了。
必须用原子操作。Yii2 的 Cache 接口不直接暴露 INCR,所以得绕一层:用 yii\redis\Connection 手动调 incr(),再用 expire() 设置过期(注意 expire 必须紧跟 incr,否则 key 可能被删掉)。
- 不要用
$cache->add()或$cache->set()模拟计数,它们不是原子的 - 窗口 Key 的过期时间要略大于窗口长度(比如 60 秒窗口设 70 秒过期),防止刚进新窗口时旧 key 还没删导致计数错乱
- 如果 Redis 不可用,建议 fail-open(放行),而不是 fail-closed(全拦),否则缓存故障会直接拖垮 API
怎么让限流规则按路由或用户角色区分
硬编码“所有接口每分钟 10 次”肯定不够用:登录接口要更宽松,管理后台接口可能要更严,VIP 用户甚至可以豁免。
关键是在行为里动态算出限流策略,而不是写死参数。比如从 $action->id 和 Yii::$app->user->identity 推导规则,再查配置表或配置数组。
- 规则配置建议用二维数组:
['login' => ['limit' => 30, 'window' => 60], 'api/v1/order/*' => ['limit' => 5, 'window' => 60]] - 路径匹配用
yii\helpers\StringHelper::matchWildcard(),别手写正则,避免回溯爆炸 - 用户角色判断别只看
isGuest,要走Yii::$app->user->can('admin')这类 RBAC 接口,确保和权限系统一致
为什么 Response 头里加 X-RateLimit 系列字段总不生效
因为 Yii2 的 Response 对象在 beforeAction 阶段还没进入输出流程,直接 setHeader 不会自动刷到 HTTP 头里——尤其你用了 exit 或 die 强制终止时,header 更不会发出去。
正确做法是:在行为里调用 Yii::$app->response->headers->set(),然后 return false;框架会在后续响应阶段真正发送这些 header。
- 必须设置三个头:
X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset(后者填窗口结束时间戳) - 别用
header()函数,它和 Yii 的响应生命周期冲突,某些 SAPI 下(如 php-fpm)可能静默失败 - 如果用了 Nginx 做前置代理,记得在 location 块里加
proxy_pass_request_headers on;,否则这些自定义头会被丢弃
真正麻烦的是时间窗口对齐和 Redis 连接异常的兜底——这两点线上最容易出问题,但日志里往往只显示“限流触发”,看不出是策略配错了,还是缓存挂了。


















