在向公網提供API供外部訪問數據時,爲了不被惡意攻擊除了token認證最好還要給API加上請求頻次限制,而在Laravel中從5.2開始框架自帶的組件Throttle就支持訪問頻次限制了,並提供了一個Throttle中間件供咱們使用,不過Throttle中間件在訪問API頻次達到限制後會返回一個HTML響應告訴你請求超頻,在應用中咱們每每更但願返回一個API響應而不是一個HTML響應,因此在文章中會提供一個自定義的中間件替換默認的Throttle中間件來實現自定義響應內容。php
頻次限制常常用在API中,用於限制獨立請求者對特定API的請求頻率。例如,若是設置頻次限制爲每分鐘1000次,若是一分鐘內超過這個限制,那麼服務器就會返回 429: Too Many Attempts.
響應。redis
一般,一個編碼良好的、實現了頻率限制的應用還會回傳三個響應頭: X-RateLimit-Limit
, X-RateLimit-Remaining
和 Retry-After
(Retry-After
頭只有在達到限制次數後纔會返回)。 X-RateLimit-Limit
告訴咱們在指定時間內容許的最大請求次數, X-RateLimit-Remaining
指的是在指定時間段內剩下的請求次數, Retry-After
指的是距離下次重試請求須要等待的時間(s)json
注意:每一個應用都會選擇一個本身的頻率限制時間跨度,Laravel應用訪問頻率限制的時間跨度是一分鐘,因此頻率限制限制的是一分鐘內的訪問次數。api
讓咱們先來看看這個中間件的用法,首先咱們定義一個路由,將中間件throttle添加到其中,throttle默認限制每分鐘嘗試60次,而且在一分鐘內訪問次數達到60次後禁止訪問:緩存
Route::group(['prefix'=>'api','middleware'=>'throttle'], function(){ Route::get('users', function(){ return \App\User::all(); }); }); 複製代碼
訪問路由/api/users時你會看見響應頭裏有以下的信息:bash
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 58服務器
若是請求超頻,響應頭裏會返回Retry-After
:markdown
Retry-After: 58 X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0app
上面的信息表示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
這些頻次數據都是存儲在cache
裏的,Laravel
默認的cache driver
是file
也就是throttle
信息會默認存儲在框架的cache
文件裏, 若是你的cache driver
換成redis那麼這些信息就會存儲在redis裏,記錄的信息其實很簡單,Throttle
會將請求對象的signature(以HTTP請求方法、域名、URI和客戶端IP作哈希)做爲緩存key記錄客戶端的請求次數。