Laravel最佳實踐 -- API請求頻率限制(Throttle中介軟體)

kevinyan發表於2018-06-20

在向公網提供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-RemainingRetry-AfterRetry-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 driverfile也就是throttle資訊會預設儲存在框架的cache檔案裡, 如果你的cache driver換成redis那麼這些資訊就會儲存在redis裡,記錄的資訊其實很簡單,Throttle會將請求物件的signature(以HTTP請求方法、域名、URI和客戶端IP做雜湊)作為快取key記錄客戶端的請求次數。

相關文章