Last active
August 10, 2022 15:44
-
-
Save demisang/a41bd6db274e1a8f9fe4983eaebdc61d to your computer and use it in GitHub Desktop.
Yii2 Api RateLimiter class with Redis storage implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace common\components\rateLimiter; | |
use Yii; | |
use yii\base\BaseObject; | |
use yii\filters\RateLimitInterface; | |
/** | |
* API rate limiter. | |
* | |
* 1. Install & Configure https://github.com/yiisoft/yii2-redis | |
* 2. Add in controller: | |
* | |
* public function behaviors() | |
* { | |
* return ArrayHelper::merge( | |
* parent::behaviors(), [ | |
* 'rateLimiter' => [ | |
* 'class' => \yii\filters\RateLimiter::className(), | |
* 'enableRateLimitHeaders' => true, | |
* 'user' => new ApiRateLimiter(), | |
* 'only' => ['only-this-action-need-limitation'], | |
* 'except' => ['not-limited-action-name'], | |
* ], | |
* ]); | |
* } | |
*/ | |
class ApiRateLimiter extends BaseObject implements RateLimitInterface | |
{ | |
public $maxRequestsPerPeriod = 30; | |
public $period = 60; | |
protected $limitReached = false; | |
/** | |
* Set max requests and period variables depend on current route | |
* | |
* @param \yii\base\Action $action | |
*/ | |
public function setLimitsByRoute($action) | |
{ | |
$routes = [ | |
// route => [maxRequests, perPeriod] | |
'site/index' => [30, 60], | |
'site/contact' => [30, 60], | |
'site/login' => [30, 60], | |
'site/signup' => [30, 60], | |
]; | |
if (isset($routes[$action->controller->route])) { | |
$route = $routes[$action->controller->route]; | |
$this->maxRequestsPerPeriod = $route[0]; | |
$this->period = $route[1]; | |
} | |
} | |
/** | |
* Returns the maximum number of allowed requests and the window size. | |
* | |
* @param \yii\web\Request $request the current request | |
* @param \yii\base\Action $action the action to be executed | |
* | |
* @return array an array of two elements. The first element is the maximum number of allowed requests, | |
* and the second element is the size of the window in seconds. | |
*/ | |
public function getRateLimit($request, $action) | |
{ | |
$this->setLimitsByRoute($action); | |
// [$this->maxRequestsPerPeriod] times per [$this->period] | |
return [$this->maxRequestsPerPeriod, $this->period]; | |
} | |
/** | |
* Loads the number of allowed requests and the corresponding timestamp from a persistent storage. | |
* | |
* @param \yii\web\Request $request the current request | |
* @param \yii\base\Action $action the action to be executed | |
* | |
* @return array an array of two elements. The first element is the number of allowed requests, | |
* and the second element is the corresponding UNIX timestamp. | |
*/ | |
public function loadAllowance($request, $action) | |
{ | |
$redis = Yii::$app->redis; | |
// Redis storage key | |
$key = $this->getRedisKey($action, $request->userIP); | |
// Minimal actual timestamp value | |
$time = time(); | |
$since = $time - $this->period; | |
// Begin commands queue | |
$redis->multi(); | |
// Remove expired values | |
$redis->zremrangebyscore($key, 0, $since - 1); | |
// Get requests count by period | |
$redis->zcount($key, $since, $time); | |
// Execute commands queue | |
$result = $redis->exec(); | |
// If error - decline request | |
if (!is_array($result) || !isset($result[1])) { | |
return [0, $time]; | |
} | |
// Count value | |
$count = (int)$result[1]; | |
if ($count + 1 > $this->maxRequestsPerPeriod) { | |
$this->limitReached = true; | |
} | |
return [$this->maxRequestsPerPeriod - $count, $time]; | |
} | |
/** | |
* Saves the number of allowed requests and the corresponding timestamp to a persistent storage. | |
* | |
* @param \yii\web\Request $request the current request | |
* @param \yii\base\Action $action the action to be executed | |
* @param int $allowance the number of allowed requests remaining. | |
* @param int $timestamp the current timestamp. | |
*/ | |
public function saveAllowance($request, $action, $allowance, $timestamp) | |
{ | |
if ($this->limitReached) { | |
$this->limitReached = false; | |
// Don't save "Too many requests" request | |
return; | |
} | |
$redis = Yii::$app->redis; | |
// Redis storage key | |
$key = $this->getRedisKey($action, $request->userIP); | |
// Begin commands queue | |
$redis->multi(); | |
// Sorted set member score is current timestamp | |
$score = $timestamp; | |
// Get random unique value for sorted set member value | |
$member = uniqid(mt_rand(0, 9)); | |
$redis->zadd($key, $score, $member); | |
// Set expire for key | |
$redis->expire($key, $this->period); | |
// Execute commands queue | |
$redis->exec(); | |
} | |
/** | |
* Get request redis sorted set key | |
* | |
* @param \yii\base\Action $action | |
* @param string $userIp | |
* | |
* @return string | |
*/ | |
protected function getRedisKey($action, $userIp) | |
{ | |
return md5('limiter:' . $action->controller->route . ':' . $userIp); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I suggest the following edit, to make this component be independent of the code that uses it.
https://gist.github.com/vilkoz/ea67bcc106e1fb18f625fe790766b05e