我以前说过,现在再说一遍,使用框架的最大好处之一是许多常见的用例或问题已经得到解决——限制请求也不例外 。
开箱即用,Laravel cms附带一个ThrottleRequests
中间件,该中间件易于配置,可提供 IP 地址在一段时间内可以发出的请求数。
例如,路由文件中的以下代码片段会将用户或 IP 地址可以发出的请求数限制为每分钟 60 次:
- Route::middleware('throttle:rate_limit,60')->group(function () {
- Route::get('/products', function () {
- //
- });
- Route::get('/locations', function () {
- //
- });
- });
您可以将其设置为3,600
默认值,然后根据特定用户的需要向上或向下调整。
但是,如果您想限制一条(或多条)特定路线而不使用这些方法中的任何一种怎么办?
在最近的一个项目中,用户能够添加 IoT 设备并获得一个唯一的 URL,他们可以将其用作 webhook 以将数据发送到我们的系统。在 URL 中使用唯一键,我需要能够限制他们可能发出的请求。
一旦你知道如何实现,这实际上很容易实现——我们只需要了解一点现有ThrottleRequests
中间件的工作原理。
如果您打开ThrottleRequests
中间件并查看该handle()
方法,我们可以看到以下内容:
- public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
- {
- $key = $this->resolveRequestSignature($request);
-
- $maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts);
-
- if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
- throw $this->buildException($key, $maxAttempts);
- }
-
- $this->limiter->hit($key, $decayMinutes * 60);
-
- $response = $next($request);
-
- return $this->addHeaders(
- $response, $maxAttempts,
- $this->calculateRemainingAttempts($key, $maxAttempts)
- );
- }
该方法的第一行是我们感兴趣的:
$key = $this->resolveRequestSignature($request);
这是ThrottleRequests
中间件找出应该用来跟踪您的请求的密钥的地方。
中间件上的方法ThrottleRequests
如下所示:
- protected function resolveRequestSignature($request)
- {
- if ($user = $request->user()) {
- return sha1($user->getAuthIdentifier());
- }
-
- if ($route = $request->route()) {
- return sha1($route->getDomain().'|'.$request->ip());
- }
-
- throw new RuntimeException('Unable to generate the request signature. Route unavailable.');
- }
如果您是经过身份验证的用户,那么它将使用id
您模型的键名(通常除非您已覆盖它)User
。否则,它将使用请求的域和 IP 地址作为密钥。
知道了这一点,我们只需要重写resolveRequestSignature()
方法来为请求派生我们自己的密钥:
app/Http/Middleware
在called中添加一个新文件CustomThrottleMiddleware
(或者你想称呼这个中间件的任何东西)或者使用 scaffold 一些新的中间件php artisan make:middleware CustomThrottleMiddleware
将内容替换为:
-
- namespace App\Http\Middleware;
-
- use Illuminate\Routing\Middleware\ThrottleRequests;
-
- class CustomThrottleMiddleware extends ThrottleRequests
- {
- protected function resolveRequestSignature($request)
- {
- //
- }
- }
我们所做的是创建我们自己的新中间件,它扩展了现有的ThrottleRequests
中间件,现在设置为覆盖该resolveRequestSignature()
方法,以便我们可以设置我们自己的密钥。
现在你只需要将你的代码添加到resolveRequestSignature()
方法中以实际返回一个键——我将在下面包括一些例子:
- protected function resolveRequestSignature($request)
- {
- // Throttle by a particular header
- return $request->header('API-Key');
- }
- protected function resolveRequestSignature($request)
- {
- // Throttle by a request parameter
- return $request->input('account_id');
- }
- protected function resolveRequestSignature($request)
- {
- // Throttle by session ID
- return $request->session();
- }
- protected function resolveRequestSignature($request)
- {
- // Throttle by IP to a particular Model in the route - where `product` is our model and this route is set to use `product` with route model binding
- return $request->route('product') . '|' . $request->ip();
- }
在我的用例中,我选择了通过多个参数进行节流。我们为用户提供的唯一 URL 对他们来说是唯一的,但对于所有相同类型的物联网设备都是相同的。例如,我们支持的物联网设备类型A
为 、B
和C
;如果您有两种类型,A
那么它们共享相同的唯一 URL。我们对此的推理对这篇文章并不重要,所以我将其省略。
每个设备被允许每 5 分钟发出一个请求,但是由于两个类型A
的设备在技术上是两个独立的设备,我们需要包括一种将两者分开的额外方法。
为实现这一目标,我们只是弄清楚我们使用的是什么类型的物联网设备,然后从那里适当地解析令牌:
- protected function resolveRequestSignature($request)
- {
- $token = $request->route()->parameter('deviceByToken');
-
- $device = IotDevice::findByToken($token)->firstOrFail();
-
- if ($device->type === 'A')
- {
- return $token . '-' . $request->input('unique_parameter_for_type_a_per_device');
- } else if ($device->type === 'B')
- {
- return $token . '-' . $request->input('unique_parameter_for_type_b_per_device');
- } if ($device->type === 'C')
- {
- return $token . '-' . $request->input('unique_parameter_for_type_c_per_device');
- }
-
- return $token;
- }
最后但同样重要的是,您只需app/Http/Kernel.php
像这样更新文件来注册中间件以供使用:
- protected $routeMiddleware = [
- //...
- 'customthrottle' => \App\Http\Middleware\CustomThrottleMiddleware::class,
- ];
然后,这允许您像这样使用新的中间件(替换 1 和 5 以满足您的需要):
- Route::middleware('customthrottle:1,5')->group(function () {
- Route::get('/products', function () {
- //
- });
- Route::get('/locations', function () {
- //
- });
- });
使用上述方法,我们能够使用每个请求中存在的唯一路由参数以及每个设备在请求的有效负载中包含的唯一标识符来限制请求