使用 Dingo API 快速构建 RESTful API(十)—— 路由访问频率限制
所谓频率限制指的是指定时间内允许特定客户端针对单个路由发起请求的次数,也可以通过节流(throttle)这个术语来描述该行为,我们可以通过一个节流器来定义时间范围和请求次数,然后在需要限制访问频率的路由上应用这个节流器进行校验,显然,在 Laravel 中可以通过中间件来实现这个操作。
在 Dingo API 中,我们有两种方式来实现路由访问频率限制,一种是通过 Laravel 框架自带的节流中间件,一种是通过 Dingo 扩展包自己实现的节流中间件,下面我们就分别通过这两种方式简单演示下如果在 Dingo API 中限制路由访问频率。
通过 Laravel 自带的节流中间件
Laravel 自带的节流中间件别名为 throttle
,我们可以在 app/Http/Kernel.php
中看到它的引入:
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
默认情况下,这个节流器限制 1 分钟内可以对应用该中间件的路由发起 60 次请求,你也可以通过传递参数到该中间件来手动设置时间范围和请求次数。比如我们限制 1 分钟内只能尝试对获取访问令牌路由发起 3 次请求,以阻止恶意用户尝试暴力破解密码,可以这样定义上篇教程注册的通过密码授权获取访问令牌路由:
$api->post('user/token', ['middleware' => 'throttle:3,1', function () {
app('request')->validate([
'email' => 'required|string',
'password' => 'required|string',
]);
$http = new \GuzzleHttp\Client();
// 发送相关字段到后端应用获取授权令牌
$response = $http->post(route('passport.token'), [
'form_params' => [
'grant_type' => 'password',
'client_id' => env('CLIENT_ID'),
'client_secret' => env('CLIENT_SECRET'),
'username' => app('request')->input('email'), // 这里传递的是邮箱
'password' => app('request')->input('password'), // 传递密码信息
'scope' => '*'
],
]);
return response()->json($response->getBody()->getContents());
}]);
我们在这个闭包路由中应用了 throttle
中间件,并限制 1 分钟只能只能访问 3 次。如果超过 3 次,则会抛出 429 Too Many Requests
异常:
此外,我们还可以在响应头中通过 X-RateLimit-*
字段获取频率限制相关数值,Limit 标识总次数、Remaining 表示剩余次数、Reset 表示有效期:
X-RateLimit-Limit: 3
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1557801344
这个路由访问频率限制对于未登录用户而言,会基于应用域名 + 客户端 IP 地址为标识限制特定客户端,对于已认证用户而言,会基于用户的唯一标识为维度限制特定用户。
更多关于 Laravel 路由访问频率限制的使用请参考官方文档。
通过 Dingo 实现的节流中间件
下面我们再来看看如何通过 Dingo 自己实现的节流中间件实现路由访问频率限制,Dingo 中对应中间件的别名是 api.throttle
,使用方式和 Laravel 节流中间件一致,只是设置中间件参数的方式有所区别,我们可以改写上述 user/token
路由定义如下:
$api->post('user/token', ['middleware' => 'api.throttle', 'limit' => 3, 'expires' => 1, function () {
...
}
注:Dingo 节流器默认 1 个小时内针对应用该中间件的路由发起 60 次请求。
如果 1 分钟内访问上述路由超过 3 次,同样会抛出 429 响应,只是默认错误消息有所不同而已:
同样,我们可以在响应头中通过 X-RateLimit-*
字段获取频率限制相关数值,字段名和含义和 Laravel 一样,这里就不再赘述了。
Dingo 节流中间件默认通过客户端 IP 地址对用户进行识别,如果你想要对其进行自定义的话,可以这么实现(将这段代码放到某个服务提供者如 AppServiceProvider
的 boot
方法中):
app(\Dingo\Api\Http\RateLimit\Handler::class)->setRateLimiter(function ($app, $request) {
if ($app['api.auth']->check()) {
return $app['api.auth']->user()->getAuthIdentifier();
} else {
return $request->getClientIp();
}
});
自定义节流器
如果以上提供的节流中间件都满足不了你的需求,还可以自定义节流器来实现更复杂的频率限制场景。在 Dingo API 中,自定义的节流器必须实现 Dingo\Api\Contract\Http\RateLimit\Throttle
接口,或者继承自 Dingo\Api\Http\RateLimit\Throttle\Throttle
基类,Dingo 内置的几个节流器都继承自该抽象类。
讲到这里我们先介绍下 Dingo 内置的几个节流器类,这些节流器定义在 vendor/dingo/api/src/Http/RateLimit/Throttle
目录下,在路由参数中指定 limit
或 expires
的情况下,使用的是 Dingo\Api\Http\RateLimit\Throttle\Route
这个节流器,如果在路由参数中通过 throttle
指定了节流器,则使用该参数指定的节流器,否则会基于配置文件 config/api.php
中的 throttling
配置项,选择一个通过节流器 match
方法匹配到的、限制最宽松的节流器。如果以上条件都不满足,则不会进行路由访问频率限制。在最后的兜底场景中,Route
节流器始终返回 true
,所以都能匹配到,而 Authenticated
只有在用户认证的情况下可以匹配到,与之相反,Unauthenticated
只有在用户未认证的情况下才能匹配到。
注:
match
方法仅仅在路由未指定throttle
、limit
及expires
参数的情况下才会执行。这个逻辑有点问题,理论上说,即使指定了throttle
参数,但是对应的匹配方法执行失败,也应该跳过校验才是。
回到自定义节流器类,对于继承自 Throttle
抽象类的自定义节流器而言,只需要实现 match
方法即可,我们在 match
方法中定义该节流器生效的匹配条件,如果匹配则返回 true
,否则返回 false
,Dingo 底册会根据返回值判断是否应用该节流器,如果想要设置自定义的客户端识别方式,还可以让节流器实现 Dingo\Api\Contract\Http\RateLimit\HasRateLimiter
接口。
接下来我们创建自定义节流器 app/Throttles/CustomThrottle.php
并编写相应的实现代码如下:
<?php
namespace App\Throttles;
use Dingo\Api\Contract\Http\RateLimit\HasRateLimiter;
use Dingo\Api\Http\RateLimit\Throttle\Throttle;
use Dingo\Api\Http\Request;
use Illuminate\Container\Container;
class CustomThrottle extends Throttle implements HasRateLimiter
{
protected $options = ['limit' => 5, 'expires' => 1];
/**
* Attempt to match the throttle against a given condition.
*
* @param \Illuminate\Container\Container $container
*
* @return bool
*/
public function match(Container $container)
{
return ! $container['api.auth']->check();
}
// 通过域名+IP识别客户端
public function getRateLimiter(Container $app, Request $request)
{
return $request->route()->getDomain() . '|' . $request->getClientIp();
}
}
在这个节流器中,我们定义用户在未认证情况下才会使用这个节流器,并且设置通过域名+IP地址对客户端进行识别,1 分钟中指定客户端可以针对应用该节流器的路由发起 5 次请求,接下来,就可以在 routes/api.php
中通过 throttle
参数指定 user/token
路由使用该自定义节流器:
$api->post('user/token', ['middleware' => 'api.throttle', 'throttle' => 'custom', function () {
...
}]);
我们还可以将该节流器配置到 config/api.php
的 throttling
配置项:
'throttling' => [
'default' => \Dingo\Api\Http\RateLimit\Throttle\Route::class,
'custom' => \App\Throttles\CustomThrottle::class,
],
这样,在用户未认证且路由未指定 limit
和 expires
参数的情况下,会使用上面两个节流器中限制较宽松的那个。
3 Comments
custom没起作用~,最终使用了['middleware' => 'api.throttle', 'throttle' => 'App\Throttles\CustomThrottle']这种方式
确实 按照本页配置 提示 custom class 不存在 使用了你这个写法就可以了
感谢,这篇文章比官方文档讲的清楚多了