Commit 64370f1a by Qiang Xue

Implemented rate limiter.

parent 93861e09
...@@ -49,6 +49,12 @@ class Controller extends \yii\web\Controller ...@@ -49,6 +49,12 @@ class Controller extends \yii\web\Controller
*/ */
public $authMethods = ['yii\rest\HttpBasicAuth', 'yii\rest\HttpBearerAuth', 'yii\rest\QueryParamAuth']; public $authMethods = ['yii\rest\HttpBasicAuth', 'yii\rest\HttpBearerAuth', 'yii\rest\QueryParamAuth'];
/** /**
* @var string|array the rate limiter class or configuration. If this is not set or empty,
* the rate limiting will be disabled.
* @see checkRateLimit()
*/
public $rateLimiter = 'yii\rest\RateLimiter';
/**
* @var string the chosen API version number * @var string the chosen API version number
* @see supportedVersions * @see supportedVersions
*/ */
...@@ -186,15 +192,26 @@ class Controller extends \yii\web\Controller ...@@ -186,15 +192,26 @@ class Controller extends \yii\web\Controller
/** /**
* Ensures the rate limit is not exceeded. * Ensures the rate limit is not exceeded.
* You may override this method to log the API usage and make sure the rate limit is not exceeded. *
* If exceeded, you should throw a [[TooManyRequestsHttpException]], and you may also send some HTTP headers, * This method will use [[rateLimiter]] to check rate limit. In order to perform rate limiting check,
* such as `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, * the user must be authenticated and the user identity object (`Yii::$app->user->identity`) must
* to explain the rate limit information. * implement [[RateLimitInterface]].
*
* @param \yii\base\Action $action the action to be executed * @param \yii\base\Action $action the action to be executed
* @throws TooManyRequestsHttpException if the rate limit is exceeded. * @throws TooManyRequestsHttpException if the rate limit is exceeded.
*/ */
protected function checkRateLimit($action) protected function checkRateLimit($action)
{ {
if (empty($this->rateLimiter)) {
return;
}
$identity = Yii::$app->getUser()->getIdentity(false);
if ($identity instanceof RateLimitInterface) {
/** @var RateLimiter $rateLimiter */
$rateLimiter = Yii::createObject($this->rateLimiter);
$rateLimiter->check($identity, Yii::$app->getRequest(), Yii::$app->getResponse(), $action);
}
} }
/** /**
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
/**
* RateLimitInterface is the interface that may be implemented by an identity object to enforce rate limiting.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
interface RateLimitInterface
{
/**
* Returns the maximum number of allowed requests and the window size.
* @param array $params the additional parameters associated with the rate limit.
* @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($params = []);
/**
* Loads the number of allowed requests and the corresponding timestamp from a persistent storage.
* @param array $params the additional parameters associated with the rate limit.
* @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($params = []);
/**
* Saves the number of allowed requests and the corresponding timestamp to a persistent storage.
* @param integer $allowance the number of allowed requests remaining.
* @param integer $timestamp the current timestamp.
* @param array $params the additional parameters associated with the rate limit.
*/
public function saveAllowance($allowance, $timestamp, $params = []);
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
use yii\base\Component;
use yii\base\Action;
use yii\web\Request;
use yii\web\Response;
use yii\web\TooManyRequestsHttpException;
/**
* RateLimiter implements a rate limiting algorithm based on the [leaky bucket algorithm](http://en.wikipedia.org/wiki/Leaky_bucket).
*
* You may call [[check()]] to enforce rate limiting.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class RateLimiter extends Component
{
/**
* @var boolean whether to include rate limit headers in the response
*/
public $enableRateLimitHeaders = true;
/**
* @var string the message to be displayed when rate limit exceeds
*/
public $errorMessage = 'Rate limit exceeded.';
/**
* Checks whether the rate limit exceeds.
* @param RateLimitInterface $user the current user
* @param Request $request
* @param Response $response
* @param Action $action the action to be executed
* @throws TooManyRequestsHttpException if rate limit exceeds
*/
public function check($user, $request, $response, $action)
{
$current = time();
$params = [
'request' => $request,
'action' => $action,
];
list ($limit, $window) = $user->getRateLimit($params);
list ($allowance, $timestamp) = $user->loadAllowance($params);
$allowance += (int)(($current - $timestamp) * $limit / $window);
if ($allowance > $limit) {
$allowance = $limit;
}
if ($allowance < 1) {
$user->saveAllowance(0, $current, $params);
$this->addRateLimitHeaders($response, $limit, 0, $window);
throw new TooManyRequestsHttpException($this->errorMessage);
} else {
$user->saveAllowance($allowance - 1, $current, $params);
$this->addRateLimitHeaders($response, $limit, 0, (int)(($limit - $allowance) * $window / $limit));
}
}
/**
* Adds the rate limit headers to the response
* @param Response $response
* @param integer $limit the maximum number of allowed requests during a period
* @param integer $remaining the remaining number of allowed requests within the current period
* @param integer $reset the number of seconds to wait before having maximum number of allowed requests again
*/
protected function addRateLimitHeaders($response, $limit, $remaining, $reset)
{
if ($this->enableRateLimitHeaders) {
$response->getHeaders()
->set('X-Rate-Limit-Limit', $limit)
->set('X-Rate-Limit-Remaining', $remaining)
->set('X-Rate-Limit-Reset', $reset);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment