7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Laravelでレートリミット制限をかけて制限時の429エラーレスポンスをjsonで返す

Last updated at Posted at 2020-06-24

Dos攻撃対策のレート制限を実装

apiの外部公開をするにあたり、Dos攻撃対策としてレートリミット機能を実装しました。

venderにあるライブラリ標準機能では、アクセス制限がかかった際は429エラーページの表示がされるようになっています。

それを次のように変更しました。

・jsonでエラーメッセージを返すようにする
・レートリミット制限の設定を環境変数から制御する

今回行った実装内容を簡単にご紹介します。

参考リンク

公式のレートリミットの解説
https://readouble.com/laravel/6.x/ja/routing.html

ルーティングの設定についての解説(今回は触りませんでした)
https://laravel.com/docs/7.x/routing#rate-limiting

分かりやすく解説されているブログの記事一覧
https://blog.pinkumohikan.com/entry/laravel-rate-limit-middleware
https://access-jp.co.jp/blogs/development/203
https://www.suzu6.net/posts/170-laravel-throttle/

middlewareにレートリミット用のファイルを作成する

app/Http/Middleware/RateLimitting/ThrottleRequests.php

middlewareにディレクトリを作り、その中にファイルを作成します

最初はvenderディレクトリから継承したThrottleRequestsExceptionを改良してレスポンスを返せないか試行錯誤していたのですが、どうも上手くいかず、、

方法を考え参考になりそうな記事を探しまくっていた所、stackoverflowにちょうどドンピシャな内容がありました。


こちらの一番最後にあるソースコードを元に、一部変更をして実装します。
https://stackoverflow.com/questions/40246741/laravel-rate-limit-to-return-a-json-payload

Create a new file ApiThrottleRequests.php in app/Http/Middleware/ and paste the code below:

変更した部分の解説を記載します。

ヘルパー関数のconfig()を使って環境変数の値からレートリミットを設定する

$maxAttempts = (int) config('app.maxAttempts');

Note:
環境変数の呼び出しでenvではなくconfigを使ったのは、 本番環境でconfig:cacheコマンドを実行すると、その後env()はnullしか返さないからです。

↓ 公式でも言及されています

レスポンスをjsonに変更


    /**
     * Create a 'too many attempts' response.
     *
     * @param  string $key
     * @param  int $maxAttempts
     * @return \Illuminate\Http\Response
     */
    protected function buildResponse($key, $maxAttempts)
    {
        $errorResponse = json_encode([
                'status_code' => '429',
                'error' => 'Too Many Requests',
                'error_description' => 'アクセス数が上限に達しました。',
        ]);

        $response = new Response($errorResponse, 429);

        $retryAfter = $this->limiter->availableIn($key);

        return $this->addHeaders(
            $response, $maxAttempts,
            $this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
            $retryAfter
        );
    }

引数の値を変えて、~分間の制限に指定(ここでは1分としています)

// $this->limiter->hit($key, $decayMinutes);
$this->limiter->hit($key, 60);

kernel.phpにパスを指定する

kernel.phpに追加したファイルのパスを指定します

※環境変数からレートリミットを制御させたいので、数値指定を削除しました

'api' => [
// 'throttle:60,1', 環境変数から制御する為に元々あった数値設定を削除
'throttle',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],

protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \App\Http\Middleware\RateLimitting\ThrottleRequests::class, //ミドルウェアのファイルパスを指定
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];

app.phpに環境変数を設定

config\app.php

環境変数の設定数値を読み込むようにします。
※説明文はオリジナルです


    /*
    |--------------------------------------------------------------------------
    | Rate Limit Access
    |--------------------------------------------------------------------------
    | Includes a middleware to rate limit access to your api.
    | The throttle middleware accepts parameters that determine the maximum
    | number of requests that can be made in one minutes. 
    | Thanks you for reading my 4th article on Qiita.
    */
    
    'maxAttempts' => env('MAX_ATTEMPTS_PER_MINUTE', '100'),

環境変数にアクセス上限数を設定する

.envに設定値を記載します

#レートリミット制限値の設定:設定した数値のアクセス数を超えると制限がかかる
MAX_ATTEMPTS_PER_MINUTE=100

テストしてみる

postmanを使ってレートリミット制限がかかっているかどうか、設定したエラー内容がjsonで返ってくるか、確かめてみます。

実際の構成では、AWSのALB(Application Load Balancer)を経由して接続させています。X-Forwarded-ForにIPアドレスが格納され、この値ごとに制限がかかるようにしています。(※本記事では関連するコードの明記を省略しています)

スクリーンショット 2020-06-24 12.07.45.png

制限(X-RateLimit-Limit)を5回にしてテストしています。一番下のX-RateLimit-Remainingが残りの試行回数です。

スクリーンショット 2020-06-24 12.08.26.png

5回以上試行すると、X-RateLimit-Remainingはゼロとなり、リミット制限が戻る時間を示すRetry-Afterが表示されるようになりました。

表示例では、残り16秒との事です。

スクリーンショット 2020-06-24 12.08.41.png

レスポンス結果は以下の通りです。Statusには429 too Many Request。Bodyもjsonで指定した通りのレスポンスが返ってくる事を確認できました。

スクリーンショット 2020-06-24 12.13.41.png

以上になります。少しでも実装の参考になれば幸いです。

HTTPのエラーページをカスタマイズ表示させたい場合は、
/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/views

のbladeで編集できますので、目的に応じて対応しましょう。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?