Skip to content

Instantly share code, notes, and snippets.

@demisang
Last active August 10, 2022 15:44
Show Gist options
  • Save demisang/a41bd6db274e1a8f9fe4983eaebdc61d to your computer and use it in GitHub Desktop.
Save demisang/a41bd6db274e1a8f9fe4983eaebdc61d to your computer and use it in GitHub Desktop.
Yii2 Api RateLimiter class with Redis storage implementation
<?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);
}
}
@vilkoz
Copy link

vilkoz commented Aug 10, 2022

I suggest the following edit, to make this component be independent of the code that uses it.
https://gist.github.com/vilkoz/ea67bcc106e1fb18f625fe790766b05e

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment