Yii2 RESTful API 限流需配置 RateLimiter 行为,支持 Redis/DB 存储、自动响应头、可自定义 key(如按 IP)、须处理 Redis 故障降级及 429 响应 JSON 格式化。

Yii2 RESTful API 里怎么加限流?直接用 RateLimiter 行为
Yii2 的 RESTful API 限流,核心就是给控制器加 RateLimiter 行为(Behavior),它不依赖外部服务,靠 Redis 或数据库存计数,开箱即用。别自己手写中间件或在 action 里 if-else 判断请求次数——既难维护,又容易绕过。
典型用法是继承 yii\rest\ActiveController,然后在 behaviors() 方法里配好:
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['rateLimiter'] = [
'class' => \yii\filters\RateLimiter::class,
'enableRateLimitHeaders' => true,
];
return $behaviors;
}
-
enableRateLimitHeaders设为true会自动返回X-Rate-Limit-Limit、X-Rate-Limit-Remaining等响应头,前端或网关可直接读取 - 默认限流策略是每分钟 60 次(
yii\filters\RateLimiter::loadAllowance()内硬编码),要改必须重写该方法或换自定义限流器 - 限流 key 默认基于用户 ID(
$user->id);未登录用户会被当作 ID=0 处理,所有游客共用一个配额——这点极易被忽略,导致“一人狂刷拖垮全站”
限流依据是 IP 还是用户?得自己改 getRateLimit() 和 loadAllowance()
Yii2 原生 RateLimiter 只认 $user->id,不看 IP、Token、Client-ID。想按 IP 限流?必须重写两个方法:
-
getRateLimit($request, $response):返回数组[maxRequests, timeWindow],比如[10, 60]表示 60 秒内最多 10 次 -
loadAllowance($request, $response):返回当前剩余配额和时间戳,关键是要在这里生成唯一 key —— 比如用$request->getUserIP()而不是$user->id
示例片段(只改 key 逻辑):
protected function loadAllowance($request, $response)
{
$key = 'rate_limit_ip_' . $request->getUserIP();
$redis = \Yii::$app->redis;
$data = $redis->executeCommand('HMGET', [$key, 'allowance', 'timestamp']);
// ... 后续解析和更新逻辑保持不变
}
注意:loadAllowance() 返回的 timestamp 必须是秒级整数(不是毫秒),否则窗口计算会错乱;Redis 连接必须提前配置好,否则抛 InvalidConfigException。
Redis 挂了限流就失效?得加降级兜底
RateLimiter 默认用 Redis 存数据,但没内置失败降级机制。一旦 Redis 不可用,executeCommand() 抛异常,整个请求直接 500——这不是限流,是拒绝服务。
- 最简兜底:在
loadAllowance()里 try-catch Redis 异常,捕获后返回宽松配额(如[PHP_INT_MAX, 1]),让请求继续走 - 更稳的做法:加个开关配置(如
rateLimiter.fallbackEnabled),异常时查本地内存缓存(yii\caching\FileCache)或直接放行 - 别依赖
yii\filters\RateLimiter::$enableRateLimitHeaders来判断是否启用限流——它只控制响应头,不控制逻辑执行
API 返回 429 却没内容?要手动设 Content-Type 和响应体
触发限流时,Yii2 默认只设状态码 429,响应体为空,Content-Type 是 text/html(因为继承自 Web 应用默认行为)。RESTful API 返回空 HTML 对前端极不友好。
- 必须在控制器里覆盖
beforeAction()或在RateLimiter::beforeAction()后补处理 - 推荐做法:在
RateLimiter行为配置里加'except' => ['options']避免预检请求被误限,再统一用Response::FORMAT_JSON - 简单有效的方式:在
behaviors()里把RateLimiter和ContentNegotiator一起配,确保 429 响应也走 JSON 格式
真正上线时,别只测“能限”,重点测“Redis 故障时是否还能响应”“未登录用户是否被正确隔离”“IP 伪造头(X-Forwarded-For)是否被信任”——这些点一漏,限流就形同虚设。










