使用 Dingo API 擴充套件包快速構建 Laravel 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
引數的情況下,會使用上面兩個節流器中限制較寬鬆的那個。