artisansdk/ratelimiter

Laravel 兼容的漏桶速率限制器和相应的中间件,具有路由级别的粒度。

1.1.0 2024-03-14 14:52 UTC

This package is auto-updated.

Last update: 2024-09-14 16:03:21 UTC


README

Laravel 兼容的漏桶速率限制器和相应的中间件,具有路由级别的粒度。

目录

安装

该软件包安装到 PHP 应用程序中,就像其他 PHP 软件包一样。

composer require artisansdk/ratelimiter

安装后,您需要将速率限制器类的 Bucket 实现绑定到您选择的内容。如果您需要额外的事件分发,请选择 LeakyLeaky Evented 桶。将以下行添加到您的 App\Providers\AppServiceProvider

use ArtisanSdk\RateLimiter\Buckets\Leaky;
use ArtisanSdk\RateLimiter\Contracts\Bucket;

public function register()
{
    $this->app->bind(Bucket::class, Leaky::class);
}

如果您打算使用 Evented 漏桶,那么您还希望在您的 register() 方法中将绑定更改为以下内容。事件调度器将由 Laravel 自动注入

use ArtisanSdk\RateLimiter\Buckets\Evented;
use ArtisanSdk\RateLimiter\Contracts\Bucket;

public function register()
{
    $this->app->bind(Bucket::class, Evented::class);
}

该软件包包括与 Laravel 内置的 Illuminate\Routing\Middleware\ThrottleRequests 兼容的速率限制器中间件。只需更新 App\Http\Kernel::$routeMiddleware 数组,使 throttle 键指向 ArtisanSdk\RateLimiter\Middleware,如下所示

protected $routeMiddleware = [
    // ...
    'throttle' => \ArtisanSdk\RateLimiter\Middleware::class,
];

现在请求将通过漏桶速率限制器。当超出限制时,请求将被根据每秒 1 个请求的速率(r/s)进行限制,最大容量为 60 个请求,超时时限为 1 分钟。这是基于默认 Laravel 签名 throttle:60,1,该签名位于 App\Http\Kernel::$middlewareGroups 下的 api 组中

protected $middlewareGroups = [
    // ...
    'api' => [
        'throttle:60,1',
        'bindings',
    ],
];

更改速率或将 throttle:60,1 添加到 web 组中,以便对常规页面请求进行速率限制。有关更多信息,包括使用中间件之外的速率限制器和桶,请参阅使用指南

使用指南

Laravel 速率限制器概述

Laravel 多年没有速率限制,因此这是一个受欢迎的添加。诚实地讲,一些速率限制比没有要好。但从安全角度来看,Laravel 的速率限制器最多只能减缓黑客,通常会使合法使用受到影响,并产生虚假的安全感。

Laravel 的实现

Laravel拥有一个固定的衰减速率限制器。默认设置throttle:60,1表示客户端在触发1分钟的强制衰减超时前,可以每分钟产生60次请求,衰减时间为1分钟。客户端可以在1秒内产生60次请求,或者以每秒1次请求(1 r/s)的速度分散在60秒内。如果请求均匀分布在大约1 r/s,则客户端不会受到速率限制。这意味着throttle:120,2实际上等同于1 r/s,但跟踪时间为2分钟,允许更大的突发限制,最高可达120次请求。同时,throttle:120,1将是一个有效的2 r/s速率,但突发限制相同。

问题 1:突发利用

通常,你希望这两个数字都很大,因为这样可以提供更多的滥用追踪,同时允许足够的合法请求。例如,如果目标是获得24小时内的平均1 r/s,最高可达100K次请求,则这相当于throttle:100000,1440。每天客户端可以在1秒内产生100K次请求!那么1 r/s的负载均衡就无足轻重了。此外,滥用没有惩罚——只需等待1440分钟,就可以像不间断地进行1 r/s一样再次进行。因此,可以将它降低到throttle:3600,60,这样突发限制就限制在3600次请求,但速率每小时重置一次。可能会有一个最佳点,但很难恰到好处。

问题 2:无粒度

此外,客户端的签名由请求者的域名和IP地址确定。虽然大多数黑客会随机化他们的IP,并且几乎所有速率限制器都受到这一点的影响(以及公共网络上常见的共享IP地址问题),但所有由用户产生的请求都会进入同一客户端的命中缓存。因此,您可以为不同的路由设置不同的限制器,例如,为用户登录屏幕设置throttle:10,10,为其他路由设置throttle:60,1,因为您听说应该根据资源的典型使用情况对资源进行速率限制。结果,您发现用户触发了速率限制,因为他们对一条路由产生了大量请求,触发了另一条路由的速率限制器。因此,您提高了限制,因为这听起来像是一个简单的解决方案,但结果您只是增加了您的攻击面。

问题 3:不可扩展

如果用户已登录,Laravel确实使用用户的唯一标识符作为键,这比IP地址更好。更好的是,为不同用户使用基于字符串的键设置不同的速率,如throttle:60|rate_limit,这相当于访客60次请求和用户(或如果您使用Laravel建议的throttle:rate_limit值,它实际上对访客将是throttle:0)返回的任何值。这些都是好的,但您希望根据用户访问的资源对资源进行不同的速率限制。解决这个问题的唯一方法是篡改Illuminate\Routing\Middleware\ThrottleRequests中间件,并重载resolveRequestSignature()方法以返回您的自定义键。哦,别忘了,访客和认证用户都使用相同的衰减速率,所以您必须处理这种无意的安全耦合。

理解漏桶算法

Laravel速率限制器的解决方案是更好的算法,包括一些额外的配置设置。

漏桶实现

漏桶算法是这个包实现的速率限制器。正如其名所示,有一个桶(缓存)可以用来滴入(请求)直到最大容量(限制),此时如果继续填充,则会溢出(速率限制)。这个桶每秒也会以恒定的速率滴漏(每秒请求)。这意味着,如果您以与桶漏出的相同速率填充滴漏,那么您可以无限期地连续击打它而不会溢出。您也可以达到最大容量,这对漏出速率没有影响。因此,漏桶算法强制执行一个恒定的滴漏速率,这个速率不由添加到桶中的滴漏数量决定,而由恒定时间内的漏出速率决定。由于该算法跟踪漏出和桶而不是仅仅滴漏,因此桶可以持久化更长时间,以跟踪恶意活动更长时间,并平衡请求负载。

解决方案 1:突发限制

如前所述,突增是一种黑客可以利用Laravel速率限制器进行的攻击,并监控(增加限制)的利用使得攻击面更大。在漏桶实现中,突增限制是一个独立的限制,它不会以二进制的方式过期,要么全部过期,要么什么也不过期,而是随着桶的漏出逐滴过期。使用这种实现,您设置突增限制,该限制在恒定漏出速率下缓慢地随着时间的推移而减少。这意味着,只要客户端不超过限制并且没有超时,客户端可以突增到这个限制,然后等待漏出速率的一段时间再发送一个请求。只要他们不超出桶的最大容量,他们就可以不断发送请求。

例如,如果设置是throttle:60,1,则用户可以在前1秒内突增到60个请求,然后只需要等待1秒就可以发送后续请求,但如果他们发送2个请求,则会溢出,这会引入超时处罚。客户端休息的时间越长,他们每秒可以发送的请求就越多。设置中的两个配置转换为突增时的最大请求量为60,以及平均每秒请求率1 r/s(技术上是一秒一个漏出或1 l/s)。现在设置更高的限制表示更高的性能,并且由于它们是相互独立的,因此配置与其效果的关系是清晰的。

解决方案 2:路由级别粒度

如前所述,Laravel内置的关键解析器用于确定唯一客户端简单地基于访客的IP地址或认证用户的标识符。没有比这更细粒度的东西,任何引入细粒度的尝试都感觉像是一种破解,或者充满了复杂性。相反,这个速率限制器附带了一些不同的解析器,包括ArtisanSdk\RateLimiter\Resolver\Route类,它尝试将客户端与其请求的特定URI的速率匹配。这是首先通过路由的名称来实现的,如果失败则回退到路由的控制方法,最后只是使用URI。如果这些回退都无法解析,它将简单地回退到默认行为,即解析为访客的IP地址或认证用户的标识符。要使用此解析器,您只需在路由绑定中设置它,如下所示throttle:ArtisanSdk\RateLimiter\Resolver\Route,60,1

速率限制器实现了一个漏桶算法,因此一个桶的粒度需要应用于用户更全局的限制。该实现使用多桶解决方案。桶的速率从外向内级联,从更全局到更具体。您可以将更细粒度的路由级速率视为更用户级速率限制的子集,因此对路由级速率的限制也视为对用户级速率的限制。如果其中任何一个被触发,则该桶的限制将生效。不同的路由共享相同的父用户级桶,但各自有自己的桶限制,因此只触发路由级桶将阻止对该路由的进一步请求,而其他路由可能仍然活跃。这种用例适用于您需要将特定资源限制在较低请求阈值的同时,限制用户的每日最大请求次数。

解决方案 3:可扩展的关键解析器

Laravel的速率限制器和这个漏桶速率限制器都使用缓存键来保存给定客户端对速率限制器的命中次数。这两个速率限制器的默认解析器相同。这些解析器定义的方式不同:此包使用一个单独的解析器类,您可以在配置路由时自定义和混合使用。该包包含三个内置解析器,您可以非常容易地创建自己的解析器。

  • 按IP/用户限制(默认):ArtisanSdk\RateLimiter\Resolvers\User
  • 按路由限制:ArtisanSdk\RateLimiter\Resolvers\Route
  • 按标签限制:ArtisanSdk\RateLimiter\Resolvers\Tag

因为User解析器是默认的,所以您根本不需要指定解析器,这使得throttlethrottle:\ArtisanSdk\RateLimiter\Resolvers\User等价。正常的绑定throttle:60,1仍然适用,只是像这样添加到末尾throttle:\ArtisanSdk\RateLimiter\Resolvers\User,60,1。要使用不同的解析器(所有默认回退到User解析器),只需将其作为第一个参数,例如throttle:\ArtisanSdk\RateLimiter\Resolvers\Route,60,1

您可以定义自己的解析器并像这样调用它们,例如throttle:\App\Http\FooBarResolver,并在自定义解析器上实现ArtisanSdk\RateLimiter\Contracts\Resolver接口。扩展性内置其中。因此,解析器也可以用来共享和重用典型的限制设置,因此不再需要在路由绑定中使用魔法数字。例如,throttle:\App\Http\UserResourceLimitsthrottle:\App\Http\HighLimitsthrottle:\App\Http\SlowLimits

附加功能:溢出惩罚

此包的实现还会对填充器(客户端)进行可定制的惩罚,如果它们溢出(超过爆发限制)。这是通过第三个配置设置实现的,例如throttle:60,1,60。默认值是60分钟,接近Laravel速率限制器的默认行为。然而,此值独立于其他所有值,而Laravel的则与衰减时间相关联。此第三个配置设置设置了客户端必须休息的秒数,才能再次发送请求。即使在根据泄漏率通常会重置爆发速率的情况下,也会强制执行此操作。例如,throttle:60,1,600将对超过60请求爆发限制实施600秒(10分钟)的超时。这将使黑客的攻击速度比Laravel内置的速率限制器慢10倍。

此外,通过使用分割配置,例如throttle:60|100,1|10,86400|3600,可以不同地自定义访客和认证用户的时间超时设置。这意味着访客一次最多可以发起60次请求或以每秒1 r/s的恒定速率发起请求;如果违反这些规则,则超出了每日限制,并且将被速率限制24小时(86400秒)。与此同时,认证用户一天内可以最多发起100次请求或以每秒10 r/s的恒定速率发起请求(每天846K次请求),如果违反规则,则只有60分钟(3600秒)的超时时间。对于登录路由,可以使用基于路由的速率限制器,例如throttle:Resolvers\Route::class,3,0.1,600,这会将登录限制为每101次请求(1r/10s --> 0.1 r/s)并且最多在10秒内发起3次请求,任何违规者将被禁止10分钟(600分钟)。

访客与认证用户的不同速率

你可能不信任访客的程度不如认证用户。实际上,你不应该信任任何人,但这个包使得为访客和认证用户分段的速率更容易。Laravel使用管道(|)分隔的约定来通过配置设置实现这一点,所以这个包扩展了这种行为。访客的值将在管道的左侧,用户的值将在管道的右侧。例如,throttle:60|120将应用对访客的60次请求限制和对用户的120次请求限制。你也可以在管道的用户端提供一个字符串,以动态地从认证用户设置速率,例如throttle:60,rate_limit或只是throttle:rate_limit,如果你对访客端推断为0没有问题。

所有这些配置设置,如maxrateduration,都可以以这种方式配置(这是Laravel速率限制器不做的),因此你可以为任何这些设置传递管道分隔的访客和用户的限制。签名格式为throttle:max|rate|duration的形式。例如,你可以这样做:throttle:60|120,1|2,300|60来设置以下限制

此外,所有用户限制都接受一个字符串,可以从认证用户处动态获取值。即使你不支持数据库驱动值,你也可以在App\User模型上创建一个属性获取器来获取值,作为一个常量或基于某些值。

不同用户的不同速率

你可能有一组分层SaaS计划、管理员和普通用户、一个比你的Web应用更频繁使用你的API的移动应用,或者只有一个似乎认为他们需要猛烈地敲击该页面的用户。换句话说,你希望为不同的用户提供不同的速率。当然,你可以将访客与用户分开,但这只是一个粗略的划分。你希望有更精细的控制,而这个包使得这更容易。你可以为每个速率设置定义每个用户的自定义速率,包括maxrateduration。只需为配置值提供一个字符串而不是数字,例如throttle:max_limit,rate_limit,duration_limit,它映射到调用例如Auth::user()->max_limitAuth::user()->rate_limit等。现在Bob和Suzy可以从他们的用户配置文件中获取不同的速率。

比这更复杂的,你可能需要使用自定义解析器。例如,如果你使用这个与TagRoute解析器一起,那么你很可能希望为不同的资源或路由设置不同的用户速率。为此,你需要实现一个速率表来设置每个用户的精细控制。在这种情况下,你必须实现一个自定义解析器来获取限制。你可以查看ArtisanSdk\RateLimiter\Resolvers\User::parse()方法以获取如何查询认证用户的限制的灵感。

处理速率限制异常

每当客户端受到速率限制时,都会抛出一个异常。这个异常是ArtisanSdk\RateLimiter\Exception,它大致对应于Illuminate\Caching\Exceptions\TooManyAttempts异常,后者同样扩展了Symfony HTTP异常,并带有适当的429 Too Many Requests代码和消息。内置的Laravel异常处理器(App\Exceptions\Handler)会捕获这些异常并返回适当的响应。只需自定义Handler::report()Handler::render()方法来以不同的方式处理异常。例如,可以将速率限制作为审计日志事件记录在用户的个人资料中,以进一步调查或报告可疑用户行为。利用这些日志作为反馈循环,甚至可以增加该用户的后续处罚率,使他们必须减速或承受越来越严重的回退处罚,直至永久阻止其用户。

为速率限制器设置自定义缓存

虽然Laravel足够智能,可以解析您的默认缓存驱动程序,但您可能希望特别使用专门的Redis或Memcached缓存来存储漏桶的击中和计时器。在这种情况下,您需要在App\Providers\AppServiceProvider类中注册该配置。只需将以下内容添加到您的register()方法中(或者更好的做法是将其抽象到自己的方法中):

use ArtisanSdk\RateLimiter\Contracts\Limiter;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

$this->app->when(Limiter::class)
    ->needs(Repository::class)
    ->give(function(){
        return Cache::driver('redis');
    });

此绑定确保当Limiter注入到中间件中时,它会使用redis驱动程序从容器中解析出来,而不是默认的file驱动程序,为Limiter所需的缓存Repository提供所需。如果您需要使用完全不同的Limiter或设置中间件内的不同默认解析器,您也可以执行类似操作。

请求签名解析器如何工作

键解析器实际上仅由ArtisanSdk\RateLimiter\Middleware类使用,它们的值作为请求限制传递给速率限制器。您可以使用解析器来获取请求键和速率限制,用于除了请求之外的其他事物,但通常您只会通过中间件使用它们进行请求节流。解析器是实现ArtisandSdk\RateLimiter\Contracts\Resolver接口的任何类。返回的值可以是静态返回的,也可以是从请求和其他类中可用的服务动态解析的。唯一不能覆盖的解析值是用于标识唯一请求的签名,即用于缓存漏桶的键。没有两个用户会有相同的桶,也没有两个路由会使用相同的桶,因为键会解析为不同的东西。

多个桶如何工作

键已进行哈希处理,但以原始形式看,它们看起来像客人的example.com|127.0.0.1或认证用户的johndoe[at]example.com。更细粒度的键,如example.com|127.0.01:/api/foo/bar,由TagRoute解析器使用冒号(:)分隔符嵌套。分隔符两边的部分分别进行哈希处理,因此您可以将它们视为client:bucket,其中对桶键的击中同时计为对客户端键的击中。超时持续时间从外到内解析,如果客户端键处于超时状态,则所有桶键都将受到速率限制。相反,如果桶键处于超时状态,其他桶可能仍然可用,父客户端键也可能仍然可用。由于同一客户端的多个桶共享相同的客户端键,桶击中的总和将超过客户端限制,从而导致客户端超时,而不仅仅是桶超时。

由于超时问题,客户端应始终保持在他们的限制范围内。尽管每次请求都会返回 X-RateLimit 头部,但应用程序开发者可能希望公开一个类似于 /api/rates 的 API 端点或部署其他自动通知客户端有关 所有 可用资源和它们各自限制的方式。此响应将包括客户端和特定资源通过剩余的滴答,包括任何实施速率限制超时的资源的适当重试时间戳和退避持续时间。这需要一种类型的全局速率存储,可以查询和解析,但超出了此包的范围。可能可以重用中间件,但可能需要实现 ArtisandSdk\RateLimiter\Contracts\Limiter 的自定义实现。

使用内置解析器

该包有几个内置解析器,默认为唯一标识用户并应用全局速率限制。所有其他解析器应回退到此解析器或创建子存储桶。这确保了解析器的更细粒度速率限制计数到用户的全局速率限制。所有内置解析器都使用相同的默认设置,包括访客和认证用户。

// Use the current user as the resolver (default)
// The following lines are all the same binding
use ArtisanSdk\RateLimiter\Resolvers\User;
Route::middleware('throttle');
Route::middleware('throttle:60,1,1');
Route::middleware('throttle:'.User::class.',60,1,60');

// Add the route to the bucket key to add more granularity
use ArtisanSdk\RateLimiter\Resolvers\Route;
Route::middleware('throttle:'.Route::class.',60,1,60');

// Add a tag to the bucket key to group related resources
use ArtisanSdk\RateLimiter\Resolvers\Tag;
Route::middleware('throttle:'.Tag::class.',foo,60,1,60');

创建自定义解析器

一个简单的自定义解析器可能是一个硬编码的 Tag 解析器版本,可以用来创建与节流相关资源的设置对象。类似以下这样就可以做到这一点

use ArtisanSdk\RateLimiter\Resolvers\User as Resolver;
use Symfony\Component\HttpFoundation\Request;

class UserResourceLimits extends Resolver
{
    protected $max = '50|100'; // 50 drips for guests, 100 drips for users
    protected $rate = '1|10'; // 1 drip per second for guests, 10 drips per second for users
    protected $duration = 3600; // 3600 second (60 minute) timeout
    protected $resource = 'user'; // resource key

    public function __construct(Request $request)
    {
        parent::__construct($request, $this->max, $this->rate, $this->duration);
    }

    public function key(): string
    {
        return parent::key().':'.$this->resource();
    }

    public function resource(): string
    {
        return $this->resource;
    }
}

然后要使用这个限制器,你只需像这样将其绑定到路由上

use App\Http\UserResourceLimits;

Route::middleware('throttle:'.UserResourceLimits::class)
    ->prefix('api/user')
    ->group(function($router){
        $router->get('/', 'UserApi@index');
        $router->get('{id}', 'UserApi@show');
    });

Route::get('/dashboard', 'Dashboard@index');

然后,每个以 /api/user 前缀的路由都会记录对 user 资源存储桶的访问,而 /dashboard 将使用默认的全局限制。访问仪表板将增加全局存储桶,而访问用户资源端点将增加 user 资源存储桶和全局存储桶。UserResourceLimits 解析器使用硬编码的值,这样只有一个可配置的地方可以自定义设置。这是故意封闭的,如果需要更可扩展的解决方案,则内置的 Tag 解析器将是一个更好的选择。

将自定义解析器设置为默认解析器

类似于可以将自定义缓存 Repository 注入到速率 Limiter 类中,第二个参数允许注入自定义的 ArtisanSdk\RateLimiter\Contracts\Resolver 实现。默认解析器是 ArtisanSdk\RateLimiter\Resolvers\User,要覆盖此解析器,请在您的 App\Providers\AppServiceProvider 类中绑定自定义解析器作为默认值。只需将以下内容添加到您的 register() 方法(或者更好的做法是将其抽象到它自己的方法中)

use ArtisanSdk\RateLimiter\Middleware;
use App\Http\FooBarResolver;

$this->app->when(Middleware::class)
    ->needs('$resolver')
    ->give(function(){
        return FooBarResolver::class;
    });

这将向中间件的构造函数中的 $resolver 变量提供完全限定的解析器类名,然后将在没有提供更具体路由绑定时作为默认值使用。在这种情况下,它提供了自定义的 App\Http\FooBarResolver 作为默认值。

独立使用速率限制器

Limiter 类可以单独使用来持久化漏桶 Bucket 实现。基本上,Limiter 类只是 Bucket 的抽象,以更好地符合“击中次数”、“限制”和“退避”的概念,这些概念通常用于速率限制请求或登录尝试。需要速率限制的不仅仅是这些。你可以限制模型的读取和写入次数,当达到一定限制时进行排队。你可以限制并行处理的作业数量。基本上,任何需要使用漏桶算法进行限制的东西都可以使用 Limiter 类作为独立速率限制器。

要使用限流器,您需要实现 Illuminate\Contracts\Cache\Repository 接口的自持久化层和一个 ArtisanSdk\RateLimiter\Bucket 实例。包含针对 Limiter 的滴落(击中)并且配置了所需的速率和限制的 Bucket,也会使用 $key 实例化,这是 Repository 服务用来持久化 Bucket 的。对于长期运行的守护进程,Bucket 可能甚至不会被持久化,这种情况下可以使用 Illuminate\Cache\ArrayStore 存储库。

use ArtisanSdk\RateLimiter\Limiter;
use ArtisanSdk\RateLimiter\Bucket;
use Illuminate\Support\Facades\Cache;

// Configure the limiter to use the default cache driver
// and persist the bucket under the key 'foo' and limit to
// 1 hit per minute or up to the maximum of 10 hits while bursting
$bucket = new Bucket($key = 'foo', $max = 10, $rate = 0.016667);
$limiter = new Limiter(Cache::store(), $bucket);

// Keep popping or queuing jobs until empty or the limit is hit
while(/* some function that gets a job */) {

    // Check that we can proceed with processing
    // This is an abstraction for checking if there's an existing timeout
    // or if the leaky bucket is now overflowing
    if( $limiter->exceeded() ) {

        // Put the bucket in a timeout until it drains
        // or you could use any arbitrary duration (or even allow for overflow)
        $seconds = $bucket->duration();
        $limiter->timeout($seconds);
        break;
    }

    // Execute the job and when the work is done, log a hit
    // Unlike the bucket which allows for multiples drips at a time,
    // a rate limiter usually only allows for a single hit at a time.
    $limiter->hit();
}

// Let the caller know when in seconds to try again
return $limiter->backoff();

如果您需要使用多个 Bucket,则可以简单地使用复合键(如 foo:bar)实例化一个 Bucket。限流器将同时应用针对 foo:barfoo 的击中速率。只需更改以下行即可:

$limiter = new Limiter(Cache::store(), new Bucket('foo:bar'));

// or let Laravel handle the cache driver dependencies with
$limiter = app(Limiter::class, ['bucket' => new Bucket('foo:bar')]);

查看 ArtisanSdk\RateLimiter\Contracts\Limiter 了解您可以调用的其他方法,或者查看具体的实现 ArtisanSdk\RateLimiter\Limiter,了解非合同、便利方法,如 reset()clear()hasTimeout() 等,这些方法是特定于实现的。

创建自定义速率限制器

如果限流器的逻辑不符合您的喜好,您可以使用 Middleware 和漏斗 Bucket,但实现您自己的 ArtisanSdk\RateLimiter\Contracts\Limiter 实例。或者,您可以通过修改引用漏斗算法的调用以使用自定义 Bucket 上的更通用方法来重新实现 Laravel 的固定衰减速率限流器。只要实现了 Limiter 合同,则可以配置 Middleware 以注入您的自定义 Limiter

类似于自定义缓存 Repository 可以注入到限流 Limiter 类中,Middleware 也可以接收您的自定义 Limiter 作为注入的依赖项。您通过在 App\Providers\AppServiceProvider 类中注册自定义 Limiter 来绑定自定义 Limiter。只需将以下内容添加到您的 register() 方法中(或者更好地,将其抽象到它自己的方法中):

use App\Http\RateLimiter;
use ArtisanSdk\RateLimiter\Contracts\Limiter;

$this->app->bind(Limiter::class, RateLimiter::class);

现在,无论在容器中解析出 Limiter 合同时的类型提示,您的自定义 RateLimiter 类都将被提供。

独立使用桶

Bucket 类可以在需要漏斗算法的任何地方使用。所有算法都是针对内部内存存储实现的,可以使用 toArray() 转换为数组,或使用 toJson() 转换为 JSON。这使得持久化层,如速率 Limiter 实现可以是任何东西。

例如,Bucket 可以实现为新的连接池和长时间运行的 WebSocket 服务器的水溢控制。每当建立新的连接时,Bucket 就会 fill() 一个滴落,当它 isFull() 时,则可以拒绝连接。由于 Bucket 持续泄漏,一旦满了,就可以以恒定的速率建立新的连接。这对于逻辑特别有用,例如,建立最多 100 个连接可以相对快速地完成,但是一旦建立了这么多连接,添加更多就涉及到更多的协调和考虑现有连接的资源优先级。拥有漏斗可以让您控制添加速率。

Bucket 具有流畅的构建器接口来配置自己,作为代码库的关键部分,值得查看其底层的原始代码。以下是它公共 API 的快速概述:

use ArtisanSdk\RateLimiter\Buckets\Leaky;

$bucket = new Leaky('foo');              // bucket named 'foo' with default capacity and leakage
$bucket = new Leaky('foo', 100, 10);     // bucket holding 100 drips that leaks 10 drips per second
$bucket = new Leaky('foo', 1, 0.016667); // bucket that overflows at more than 1 drip per minute

(new Leaky('foo'))
    ->configure([
        'max' => 100,            // 100 drips capacity
        'rate' => 10,            // leaks 10 drips per second
        'drips' => 50,           // already half full
        'timer' => time() - 10,  // created 10 seconds ago
    ])
    ->fill(10)                   // add 10 more drips
    ->leak()                     // recalculate the bucket's state
    ->toArray();                 // get array representation for persistence

$bucket = (new Leaky('foo'))     // instantiate the same bucket as above
    ->max(100)                   // $bucket->max() would return 100
    ->rate(10)                    // $bucket->rate() would return 10
    ->drips(50)                  // $bucket->drips() would return 50
    ->timer(time() - 10)         // $bucket->timer() would get the time
    ->fill(10)                   // $bucket->remaining() would return 40
    ->leak();                    // $bucket->drips() would return 30

$bucket->isEmpty();              // false
$bucket->isFull();               // false
$bucket->duration();             // 10 seconds till empty again
$bucket->key();                  // string('foo')
$bucket->reset();                // keeps configuration but reset drips and timer

使用事件桶

如果您考虑,桶中的滴落代表应用程序中发生的一些事件。在某个时刻,您将调用路由到桶中记录滴落。您可能可以监听原始事件,但如果您是通过命令总线分派,那么您可能需要将调用桶作为事件记录,以便应用程序的其他部分可以监听。

注意: Evented 桶是 Leaky 桶的扩展,它仅封装父类的事件。其他所有构建逻辑和行为都是相同的。

您可以通过将接口绑定到您的 App\Providers\AppServiceProviderregister() 方法,从基本的 Leaky 桶切换到 Evented 桶。

use ArtisanSdk\RateLimiter\Buckets\Evented;
use ArtisanSdk\RateLimiter\Contracts\Bucket;

$this->app->bind(Bucket::class, Evented::class);

然后您可以监听以下事件:

  • ArtisanSdk\RateLimiter\Events\Filling
  • ArtisanSdk\RateLimiter\Events\Filled
  • ArtisanSdk\RateLimiter\Events\Leaking
  • ArtisanSdk\RateLimiter\Events\Leaked

如果您希望在限制器超出时触发事件,您需要在您的代码中实现此操作或修改 Limiter 以触发事件。您可以通过将可选的 Illuminate\Contracts\Event\Dispatcher 实现注入到构造函数中来实现这一点。如果存在,则 Limiter 将为 Limiter::hit()Limiter::timeout() 以及可选的 Limiter::clear() 方法触发事件。

记录桶中的滴答声

另一种方法是在 Limiter 层面上装饰 Bucket,或者简单地直接在那里进行事件化。如果您注意的话,LimiterBucket 的持久化管理器,而 Bucket 只是简单地持有内存中的状态。因此,考虑到这一点,您也可以修改 Limiter,使其 hit() 方法传递一个事件对象给 Bucket,该对象被推送到事件内部堆栈中,而不是递增内部计数器。然后当 Bucket 持久化时,它不仅可以返回 $drips 的计数,还可以返回事件对象的数组。这打开了仅当它们是唯一的时记录击打的日志,或者根据收到的击打类型进一步限制桶的能力。您甚至可以根据传递给 hit() 方法的事件对象来解析正确的速率桶以存储击打。通过一点修改,您可以将 Bucket 转换为事件存储。

运行测试

该软件包经过100%的行覆盖率和路径覆盖率进行单元测试。您可以通过简单地克隆源代码、安装依赖项,然后运行 ./vendor/bin/phpunit 来运行测试。此外,开发依赖项还包括一些Composer脚本,可以帮助进行代码格式化和覆盖率报告。

composer test
composer fix
composer report

有关执行和报告输出的更多详细信息,请参阅 composer.json

许可协议

版权所有 (c) 2018-2024 Artisan Made, Co.

该软件包根据MIT许可证发布。有关商业许可条款,请参阅随代码副本分发的 LICENSE 文件。