在向公網提供API供外部訪問數據時,為了避免被惡意攻擊除了token認證最好還要給API加上請求頻次限制,而在Laravel中從5.2開始框架自帶的組件Throttle就支持訪問頻次限制了,並提供了一個Throttle中間件供我們使用,不過Throttle中間件在訪問API頻次達到限制后會返回一個HTML響應告訴你請求超頻,在應用中我們往往更希望返回一個API響應而不是一個HTML響應,所以在文章中會提供一個自定義的中間件替換默認的Throttle中間件來實現自定義響應內容。
訪問頻次限制概述
頻次限制經常用在API中,用於限制獨立請求者對特定API的請求頻率。例如,如果設置頻次限制為每分鍾1000次,如果一分鍾內超過這個限制,那么服務器就會返回 429: Too Many Attempts.響應。
通常,一個編碼良好的、實現了頻率限制的應用還會回傳三個響應頭: X-RateLimit-Limit, X-RateLimit-Remaining和 Retry-After(Retry-After頭只有在達到限制次數后才會返回)。 X-RateLimit-Limit告訴我們在指定時間內允許的最大請求次數, X-RateLimit-Remaining指的是在指定時間段內剩下的請求次數, Retry-After指的是距離下次重試請求需要等待的時間(s)
注意:每個應用都會選擇一個自己的頻率限制時間跨度,Laravel應用訪問頻率限制的時間跨度是一分鍾,所以頻率限制限制的是一分鍾內的訪問次數。
使用Throttle中間件
讓我們先來看看這個中間件的用法,首先我們定義一個路由,將中間件throttle添加到其中,throttle默認限制每分鍾嘗試60次,並且在一分鍾內訪問次數達到60次后禁止訪問:
Route::group(['prefix'=>'api','middleware'=>'throttle'], function(){
Route::get('users', function(){
return \App\User::all();
});
});
訪問路由/api/users時你會看見響應頭里有如下的信息:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
如果請求超頻,響應頭里會返回Retry-After:
Retry-After: 58
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
上面的信息表示58秒后頁面或者API的訪問才能恢復正常。
定義頻率和重試等待時間
頻率默認是60次可以通過throttle中間件的第一個參數來指定你想要的頻率,重試等待時間默認是一分鍾可以通過throttle中間件的第二個參數來指定你想要的分鍾數。
Route::group(['prefix'=>'api','middleware'=>'throttle:5'],function(){
Route::get('users',function(){
return \App\User::all();
});
});//頻次上限5
Route::group(['prefix'=>'api','middleware'=>'throttle:5,10'],function(){
Route::get('users',function(){
return \App\User::all();
});
});//頻次上限5,重試等待時間10分鍾
自定義Throttle中間件,返回API響應
在請求頻次達到上限后Throttle除了返回那些響應頭,返回的響應內容是一個HTML頁面,頁面上告訴我們Too Many Attempts。在調用API的時候我們顯然更希望得到一個json響應,下面提供一個自定義的中間件替代默認的Throttle中間件來自定義響應信息。
首先創建一個ThrottleRequests中間件: php artisan make:middleware ThrottleRequests.
將下面的代碼拷貝到app/Http/Middlewares/ThrottleReuqests文件中:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
use Symfony\Component\HttpFoundation\Response;
class ThrottleRequests
{
/**
* The rate limiter instance.
*
* @var \Illuminate\Cache\RateLimiter
*/
protected $limiter;
/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
*/
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $maxAttempts
* @param int $decayMinutes
* @return mixed
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
return $this->buildResponse($key, $maxAttempts);
}
$this->limiter->hit($key, $decayMinutes);
$response = $next($request);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
/**
* Resolve request signature.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function resolveRequestSignature($request)
{
return $request->fingerprint();
}
/**
* Create a 'too many attempts' response.
*
* @param string $key
* @param int $maxAttempts
* @return \Illuminate\Http\Response
*/
protected function buildResponse($key, $maxAttempts)
{
$message = json_encode([
'error' => [
'message' => 'Too many attempts, please slow down the request.' //may comes from lang file
],
'status_code' => 4029 //your custom code
]);
$response = new Response($message, 429);
$retryAfter = $this->limiter->availableIn($key);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
}
/**
* Add the limit header information to the given response.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* @param int $maxAttempts
* @param int $remainingAttempts
* @param int|null $retryAfter
* @return \Illuminate\Http\Response
*/
protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
{
$headers = [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
];
if (!is_null($retryAfter)) {
$headers['Retry-After'] = $retryAfter;
$headers['Content-Type'] = 'application/json';
}
$response->headers->add($headers);
return $response;
}
/**
* Calculate the number of remaining attempts.
*
* @param string $key
* @param int $maxAttempts
* @param int|null $retryAfter
* @return int
*/
protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
{
if (!is_null($retryAfter)) {
return 0;
}
return $this->limiter->retriesLeft($key, $maxAttempts);
}
}
然后將app/Http/Kernel.php文件里的:
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
替換成:
throttle' => \App\Http\Middleware\ThrottleRequests::class,
就大功告成了。
Throttle信息存儲
最后再來說下,Throttle這些頻次數據都是存儲在cache里的,Laravel默認的cache driver是file也就是throttle信息會默認存儲在框架的cache文件里, 如果你的cache driver換成redis那么這些信息就會存儲在redis里,記錄的信息其實很簡單,Throttle會將請求對象的signature(以HTTP請求方法、域名、URI和客戶端IP做哈希)作為緩存key記錄客戶端的請求次數。
