使用 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 异常:

Dingo API 路由访问频率限制

此外,我们还可以在响应头中通过 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 响应,只是默认错误消息有所不同而已:

Dingo API路由访问频率限制

同样,我们可以在响应头中通过 X-RateLimit-* 字段获取频率限制相关数值,字段名和含义和 Laravel 一样,这里就不再赘述了。

Dingo 节流中间件默认通过客户端 IP 地址对用户进行识别,如果你想要对其进行自定义的话,可以这么实现(将这段代码放到某个服务提供者如 AppServiceProviderboot 方法中):

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 目录下,在路由参数中指定 limitexpires 的情况下,使用的是 Dingo\Api\Http\RateLimit\Throttle\Route 这个节流器,如果在路由参数中通过 throttle 指定了节流器,则使用该参数指定的节流器,否则会基于配置文件 config/api.php 中的 throttling 配置项,选择一个通过节流器 match 方法匹配到的、限制最宽松的节流器。如果以上条件都不满足,则不会进行路由访问频率限制。在最后的兜底场景中,Route 节流器始终返回 true,所以都能匹配到,而 Authenticated 只有在用户认证的情况下可以匹配到,与之相反,Unauthenticated 只有在用户未认证的情况下才能匹配到。

注:match 方法仅仅在路由未指定 throttlelimitexpires 参数的情况下才会执行。这个逻辑有点问题,理论上说,即使指定了 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.phpthrottling 配置项:

'throttling' => [
    'default' => \Dingo\Api\Http\RateLimit\Throttle\Route::class,
    'custom' => \App\Throttles\CustomThrottle::class,
],

这样,在用户未认证且路由未指定 limitexpires 参数的情况下,会使用上面两个节流器中限制较宽松的那个。


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 使用 Dingo API 快速构建 RESTful API(九)—— API 认证实现(下)

>> 下一篇: 使用 Dingo API 快速构建 RESTful API(十一)—— 在应用内部请求 Dingo API