thamtech/yii2-ratelimiter-advanced

高级请求速率限制器

资助包维护!
Liberapay

安装数量: 108 816

依赖项: 1

建议者: 0

安全性: 0

星标: 38

关注者: 3

分支: 1

公开问题: 3

类型:yii2-extension

0.5 2020-08-05 04:29 UTC

This package is auto-updated.

Last update: 2024-09-10 04:35:25 UTC


README

高级速率限制器是 Yii2 的过滤器,用于强制或监控请求速率限制。

与 Yii2 内置的 RateLimiter 相比,高级速率限制器

  • 允许您根据控制器动作定义多个、独立的速率限制定义
    • 通过标识符,如 IP 地址、用户 ID、与您的应用程序相关的其他标识符或它们的组合;
  • 支持自定义对已检查或超出速率限制的响应类型,例如
    • 发送 429 Too Many Requests HTTP 响应,
    • 触发 Yii2 事件
    • 设置速率限制 HTTP 头,以及/或
    • 执行您的自己的 Callable匿名函数
  • 支持存储和管理每个速率限制的 allowancetimestamp 值(Yii2 的内置 RateLimiter 需要您自行实现存储);
  • 允许您自定义速率限制 HTTP 头的名称前缀,而不是内置 RateLimiter 使用的硬编码的 X-Rate-Limit- 前缀;
  • 支持发送指示客户端应在多少秒后重试的 Retry-After HTTP 头。

有关许可证信息,请检查 LICENSE-文件。

安装

安装此扩展的首选方式是通过 composer

php composer.phar require --prefer-dist thamtech/yii2-ratelimiter-advanced

或添加

"thamtech/yii2-ratelimiter-advanced": "*"

到您的 composer.json 文件的 require 部分。

使用方法

简介

此速率限制器是 漏桶算法 的实现。

通常,您会将速率限制器配置为任何您想要速率限制的控制器类的行为。例如,

<?php
public function behaviors()
{
    $behaviors = parent::behaviors();
    $behaviors['rateLimiter'] = [
        'class' => 'thamtech\ratelimiter\RateLimiter',
        'components' => [
            'rateLimit' => [
                'definitions' => [
                    'ip' => [
                        'limit' => 1000, // allowed hits per window
                        'window' => 3600, // window in seconds
                        
                        // Callable or anonymous function returning some unique
                        // identifier. A separate allowance will be tracked for
                        // each identifier.
                        // 
                        // Leave unset to make such a rate apply globally
                        // to all requests coming in through the controller.
                        // 
                        // @param \thamtech\ratelimiter\Context $context the current
                        //     request/action context
                        // 
                        // @param string $rateLimitId The array key that defined the
                        //     rate limit ("ip" in this case)
                        'identifier' => function($context, $rateLimitId) {
                            return $context->request->getUserIP();
                        }
                    ],
                ],
            ],
            'allowanceStorage' => [
                'cache' => 'cache', // use Yii::$app->cache component
            ],
        ],
        'as rateLimitHeaders' => [
            'class' => 'thamtech\ratelimiter\handlers\RateLimitHeadersHandler',
            
            // This can be a single string prefix, or an array of strings to duplicate
            // the headers with multiple prefixes.
            // The default prefix is 'X-Rate-Limit-' if this property is not specified
            'prefix' => ['X-Rate-Limit-', 'X-RateLimit-'],
        ],
        'as retryAfterHeader' => 'thamtech\ratelimiter\handlers\RateLimitHeadersHandler',
        'as tooManyRequestsException' => 'thamtech\ratelimiter\handlers\TooManyRequestsHttpExceptionHandler',
    ];
    return $behaviors;
}

高级示例

<?php
public function behaviors()
{
    $behaviors = parent::behaviors();
    $behaviors['rateLimiter'] = [
        'class' => 'thamtech\ratelimiter\RateLimiter',
        
        // except and only work to limit the controller actions on which the
        // rate limiter applies
        'only' => ['login', 'register', 'info'],
        'except' => ['info'],
        
        'components' => [
            'rateLimit' => [
                // class explicitly set, but defaults to this value otherwise
                // 
                // you could provide your own implementation of
                // RateLimitProviderInterface instead
                'class' => 'thamtech\ratelimiter\limit\DefaultRateLimitProvider',
                
                'definitions' => [
                    'user' => 'app\models\User', // implements RateLimitInterface
                    
                    'ip' => [
                        'class' => 'thamtech\ratelimiter\limit\RateLimit',
                        'limit' => 1000, // allowed hits per window
                        'window' => 3600, // window in seconds
                        
                        // Callable or anonymous function returning some unique
                        // identifier. A separate allowance will be tracked for
                        // each identifier.
                        // 
                        // Leave unset to make such a rate apply globally
                        // to all requests coming in through the controller.
                        // 
                        // @param \thamtech\ratelimiter\Context $context the current
                        //     request/action context
                        // 
                        // @param string $rateLimitId The array key that defined the
                        //     rate limit ("ip" in this case)
                        'identifier' => function($context, $rateLimitId) {
                            return $context->request->getUserIP();
                        }
                    ],
                    
                    'user-admin' => [
                        'limit' => 1000,
                        'window' => 3600,
                        'identifier' => Yii::$app->user->getIdentity()->id,
                        
                        // make a rate limit only be considered under certain conditions
                        'active' => Yii::$app->user->getIdentity()->isAdmin(),
                    ],
                ],
            ],
            'allowanceStorage' => [
                'cache' => 'cache', // use Yii::$app->cache component
                
                // The cache key will be made up of:
                //   {cacheKeyPrefix - defaults to 'allowance'}
                //   AllowanceCacheStorage::className() {or other storage component you might use}
                //   RateLimiterComponent::className()
                //   {your controller class}::className()
                //   {rate limit id, like "ip" or "User" in this example}
                //   {identifier, like 192.168.1.1 in this example}
                //   
                // The combination above already makes the key fairly specific to the
                // desired scope, so you probably don't need to do anything
                // special with this default value in most cases.
                'cacheKeyPrefix' => 'allowance',
            ],
        ],
        'as rateLimitHeaders' => [
            'class' => 'thamtech\ratelimiter\handlers\RateLimitHeadersHandler',
            
            // list of rateLimits to ignore
            'except' => ['user'],
            
            // This can be a single string prefix, or an array of strings to duplicate
            // the headers with multiple prefixes.
            // The default prefix is 'X-Rate-Limit-' if this property is not specified
            'prefix' => ['X-Rate-Limit-', 'X-RateLimit-'],
        ],
        
        'as retryAfterHeader' => [
            'class' => 'thamtech\ratelimiter\handlers\RetryAfterHeaderHandler',
            
            // default's to 'Retry-After' if not set
            'header' => 'Retry-After',
        ],
        
        'as tooManyRequestsException' => [
            'class' => 'thamtech\ratelimiter\handlers\TooManyRequestsHttpExceptionHandler',
            
            // list of rateLimits this handler should apply to
            'only' => ['ip'],
            
            // defaults to 'Rate limit exceeded.' if not set
            'message' => 'There were too many requests',
        ],
    ];
    return $behaviors;
}

存储

为了分配一个或多个速率限制,您必须能够存储每个配置的速率限制的整数 allowance 值和 timestamp

默认缓存中的允许量存储

速率限制器使用 allowanceStorage 组件来存储速率限制值。默认情况下,AllowanceCacheStorage 组件将允许数据存储在您指定的缓存组件中。如果没有指定缓存,将使用 yii\caching\DummyCache 的实例。

您可以通过以下几种方式指定缓存组件

<?php
...
'allowanceStorage' => [
    // EXAMPLE `cache` definitions:
    
    // as a string referencing an application cache component
    'cache' => 'cache', // refers to the Yii::$app->cache component
    
    // as a string referencing a Cache implementation class that
    // needs no configuration
    'cache' => 'app\some\implementation\of\Cache',
    
    // as a configuration array specifying a Cache class and
    // necessary configuration settings
    'cache' => [
        'class' => 'yii\caching\DbCache',
        'cacheTable' => 'allowance_cache',
    ],
    
    // or as an already-instantiated Cache object
    'cache' => Yii::createObject([
        'class' => 'yii\caching\MemCache',
        'servers' => [
            [
                'host' => 'server1',
                'port' => 11211,
                'weight' => 60,
            ],
            [
                'host' => 'server2',
                'port' => 11211,
                'weight' => 40,
            ],
        ],
    ]),
],

实现您自己的存储层

但是,您可以通过实现 AllowanceStorageInterface 并在 RateLimiterallowanceStorage 组件中引用您的实现来使用自己的存储层实现。

例如

<?php
...
'components' => [
    // EXAMPLE `allowanceStorage` definitions:
    
    // as a string referencing an AllowanceStorageInterface implementation class
    // that needs no configuration
    'allowanceStorage' => 'app\components\MyAllowanceStorage',
    
    // as a configuration array specifying an AllowanceStorageInterface implementation class
    // and necessary configuration settings
    'allowanceStorage' => [
        'class' => 'app\components\MyAllowanceStorage',
        'prefix' => 'my_allowances',
        'tag' => 'my_controller_id',
    ],
],
...

定义速率限制

单个 RateLimiter 可以定义一个或多个不同的速率限制。例如,您可能希望为每个 IP 地址提供一个速率限制,并为每个认证用户提供不同的速率限制,特别是在这不总是 一对一映射的情况下。

简单的速率限制可以定义如下

<?php
'components' => [
    'rateLimit' => [
        'ip' => [
            'limit' => 1000, // allowed hits per window
            'window' => 3600, // window in seconds
            
            // Callable or anonymous function returning some unique
            // identifier. A separate allowance will be tracked for
            // each identifier.
            // 
            // Leave unset to make such a rate apply globally
            // to all requests coming in through the controller
            'identifier' => function($context, $rateLimitId) {
                return $context->request->getUserIP();
            }
        ],
    ],
],

通过返回一个 identifier 值,您可以在每个标识符的基础上强制执行定义的速率限制,例如每个 IP 地址。或者,您也可以不指定标识符,以在全球范围内应用定义的速率限制。

您还可以通过引用 RateLimitInterface 的实现来实现您自己的速率限制定义

<?php
'components' => [
    'rateLimit' => [
        'user' => [
            'class' => 'app\models\User', // implements RateLimitInterface
        ],
    ],
],

您需要实现 getRateLimit($context) 方法以返回一个 RateLimit 对象或其属性数组(limitwindow,以及可选的标识符如用户 ID,以及可选的布尔值 active)。

定义响应

对超过速率限制可以有任意数量的响应。可以预先定义一些响应类型,作为 RateLimiter 的行为。例如

<?php
...
'as rateLimitHeaders' => [
    'class' => 'thamtech\ratelimiter\handlers\RateLimitHeadersHandler',
],

'as retryAfterHeader' => [
    'class' => 'thamtech\ratelimiter\handlers\RetryAfterHeaderHandler',
],

'as tooManyRequestsException' => [
    'class' => 'thamtech\ratelimiter\handlers\TooManyRequestsHttpExceptionHandler',
],
...

带有一些额外配置选项的更高级示例

<?php
...
'as rateLimitHeaders' => [
    'class' => 'thamtech\ratelimiter\handlers\RateLimitHeadersHandler',
            
    // list of rateLimits to ignore
    'except' => ['user'],
    
    // single string prefix, or an array of strings to duplicate
    // the headers with multiple prefixes.
    // Default prefix is 'X-Rate-Limit-' if this property is not specified
    'prefix' => ['X-Rate-Limit-', 'X-RateLimit-'],
],

'as retryAfterHeader' => [
    'class' => 'thamtech\ratelimiter\handlers\RetryAfterHeaderHandler',
    
    // defaults to 'Retry-After' if not set
    'header' => 'Retry-After',
],

'as tooManyRequestsException' => [
    'class' => 'thamtech\ratelimiter\handlers\TooManyRequestsHttpExceptionHandler',
    
    // list of rateLimits this handler should apply to
    'only' => ['ip'],
    
    // defaults to 'Rate limit exceeded.' if not set
    'message' => 'There were too many requests',
],
...

您还可以将您自己的事件处理程序附加到 RateLimiter 对象上,以查找 RateLimiter::EVENT_RATE_LIMITS_EXCEEDEDRateLimiter::EVENT_RATE_LIMITS_CHECKED 事件

<?php
...
'on rateLimitsExceeded' => function($event) {
    Yii::info('Rate limits exceeded: ' . $event->rateLimit);
},
...

或者,您可以使用 on() 方法附加事件处理程序

<?php
use thamtech\ratelimiter\RateLimiter;
$rateLimiter = $controller->getBehavior('rateLimiter');
$rateLimiter->on(RateLimiter::EVENT_RATE_LIMITS_EXCEEDED, [$this, 'onRateLimitExceeded']);

有关附加事件处理程序的信息,请参阅 Yii 的事件页面

过滤事件

如果处理程序的 onlyexcept 属性不足以进行过滤,则您的自定义事件处理程序可以将 $event->handled 设置为 true,以防止调用任何其他处理程序。

例如,如果您想将一个 IP 地址列入白名单

<?php
...
'on rateLimitsExceeded' => function($event) {
    if ($event->context->request->getUserIp() == '127.0.0.1') {
        $event->handled = true;
    }
    // other handlers will not be invoked when IP is 127.0.0.1
}
...

事件

RateLimitsCheckedEvent

每当检查一组定义的速率限制时,都会触发 RateLimitsCheckedEvent。当定义的速率限制中有一个或多个被超过时,会触发 RateLimitsExceededEvent。预定义的响应作为行为附加到 RateLimiter 上,所有这些响应都是通过将自己注册为事件处理程序并查找这些事件来工作的。

这两个事件都包含以下属性

  • $context - 包含 yii\web\Request$action 的上下文对象
  • $time - 当前的 Unix 时间戳
  • $rateLimits - 一个 RateLimitResult 对象的数组。只有超过的速率限制包含在 RateLimitsExceededEvent 中。

预定义的响应,如 RateLimitHeadersHandler,将使用速率信息来在 HTTP 头中输出适当的值。在 RateLimitHeadersHandler 的情况下,多个速率限制的信息可能被组合在一起,以计算一组 HTTP 头值。

其他响应,如 TooManyRequestsHttpExceptionHandler,不会关心是否或多少速率限制被超过。无论发生什么情况,它都将抛出 TooManyRequestsHttpException

一些响应(如 RateLimitHeadersHandler)在检查速率限制时触发,它将将其 HTTP 头添加到每个响应中,以向客户端指示其速率限制的情况。

其他响应(如 TooManyRequestsHttpExceptionHandler)仅在速率限制被超过时应用。

响应的顺序

响应添加的顺序很重要。

响应处理程序按它们附加到 RateLimiter 的顺序执行。这意味着像 TooManyRequestsHttpException 这样的异常抛出处理程序必须放在像 RateLimitHeadersHandler 这样的设置头处理程序之后。

菜谱

请参阅 菜谱

另请参阅