Laravel 13のNamed Rate Limiterでレート制限を実装する:IP・ユーザー・入力値ごとの設定
作成日:2026.09.19
LaravelのNamed Rate Limiterを使い、無制限に繰り返されたくない処理へレート制限を追加する方法を解説します。RateLimiter::for()とthrottleミドルウェアの基本から、IPアドレス・ユーザーID・入力値による制限、複数の制限、429レスポンス、ログイン処理への応用、Redis利用時の注意点まで整理します。
目次
Webアプリケーションには、利用者が何度も実行できると困る処理があります。例えば、ログイン、メールOTPの発行、重い検索、外部APIを呼び出す処理などです。
このような処理を無制限に繰り返せる状態にしておくと、アプリケーションや外部サービスへ負荷がかかったり、総当たり攻撃や不正利用につながったりする可能性があります。
Laravelには、ルートへレート制限を追加するための仕組みが用意されています。今回はNamed Rate Limiterを使い、処理ごとに名前を付けたレート制限を定義して、ルートへ適用する方法を確認します。
主な例は特定の機能に限定せず、「無制限に繰り返されたくない処理」を一般化したものにします。最後に、ログイン処理へ応用する場合のキー設計も紹介します。
前提
今回の対象環境は以下の通りです。
- OS: Windows 11
- PHP: 8.3.15
- Laravel Framework: 13.24.0
- ターミナル: PowerShell
Laravelプロジェクトを作成し、ローカルでルートへアクセスできる状態から始めます。
レート制限とは
レート制限は、一定時間内に実行できる回数へ上限を設ける仕組みです。
例えば「1分間に10回まで」という制限を設定すると、同じ制限単位から11回目のリクエストが送られたときに、処理を実行せずに拒否できます。
ここで注意したいのは、回数と期間だけを決めればよいわけではないことです。次のどの単位で数えるかによって、動作が変わります。
| 制限単位 | 特徴 | 向いている例 |
|---|---|---|
| IPアドレス | 未ログインの利用者にも適用しやすい | 公開API、ログイン画面 |
| ユーザーID | 利用者ごとに制限を分けられる | ログイン後の重い処理 |
| 入力値 | メールアドレスなど、対象の値ごとに制限できる | ログイン試行、OTP発行 |
IPアドレスだけで制限すると、同じネットワークを使う利用者が同じ制限に入る場合があります。反対に、ユーザーIDだけで制限すると、未ログイン状態の攻撃を分けて制限できません。
そのため、処理の性質に合わせて、IPアドレスとユーザーIDを使い分けたり、複数の制限を組み合わせたりします。
固定値のthrottleを使う方法
ルートへ一定回数の制限を直接指定する場合は、次のような書き方があります。
use Illuminate\Support\Facades\Route;
Route::post('/repeatable-action', function () {
return response()->json([
'message' => '処理を実行しました。',
]);
})->middleware('throttle:60,1');
この例では、ルートへ1分あたり60回という制限を指定しています。単純な制限なら短く書けますが、ルートの定義へ回数が直接書かれるため、処理ごとに条件を変えたり、制限単位を切り替えたりする場合は管理しにくくなります。
処理ごとの制限を名前付きで定義したい場合は、Named Rate Limiterを使います。
Named Rate Limiterを定義する
Laravelの公式ドキュメントでは、アプリケーションのApp\Providers\AppServiceProviderにあるbootメソッドでRate Limiterを定義しています。
app/Providers/AppServiceProvider.phpへ、次のように記述します。
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
//
}
public function boot(): void
{
RateLimiter::for('repeatable-action', function (Request $request): Limit {
return Limit::perMinute(10)->by(
'ip:' . $request->ip(),
);
});
}
}
RateLimiter::for()の第1引数がレートリミッターの名前です。ここではrepeatable-actionとしています。
クロージャーでは、Limit::perMinute(10)によって1分あたり10回の制限を作り、by()で制限単位を指定しています。上の例では、IPアドレスごとに回数を数えます。
10回という値は説明用の例です。実際の制限値は、処理にかかる負荷、利用者の操作頻度、外部サービスの上限、不正利用のリスクなどを考慮して決めます。
ルートへNamed Rate Limiterを適用する
定義したNamed Rate Limiterは、throttle:名前というミドルウェアでルートへ適用します。
routes/api.phpへ、次のルートを追加します。
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/repeatable-action', function (Request $request) {
return response()->json([
'message' => '処理を実行しました。',
]);
})->middleware('throttle:repeatable-action');
throttle:repeatable-actionの部分が、RateLimiter::for()で指定した名前と対応しています。
複数のルートへ同じ制限を適用する場合は、ルートグループへ指定できます。
Route::middleware('throttle:repeatable-action')->group(function (): void {
Route::post('/reports/refresh', [ReportController::class, 'refresh']);
Route::post('/reports/export', [ReportController::class, 'export']);
});
同じ制限を適用するルートをまとめておくと、制限の名前を変更したり、回数を見直したりするときに修正箇所を減らせます。
IPアドレス、ユーザーID、入力値で制限する
by()へ渡す値を変えると、同じレートリミッターでも制限単位を変えられます。
IPアドレス単位で制限する
RateLimiter::for('repeatable-action', function (Request $request): Limit {
return Limit::perMinute(10)->by(
'ip:' . $request->ip(),
);
});
未ログインの利用者にも適用しやすい方法です。一方で、会社や学校など、複数の利用者が同じグローバルIPアドレスを共有する環境では、利用者同士が同じ制限に入る可能性があります。
プロキシやロードバランサーの背後で運用する場合は、アプリケーションが正しいクライアントIPアドレスを取得できる設定になっているかも確認します。受け取ったヘッダーを無条件に信頼すると、制限単位を偽装されるおそれがあります。
認証済みユーザー単位で制限する
RateLimiter::for('repeatable-action', function (Request $request): Limit {
$user = $request->user();
if ($user === null) {
return Limit::perMinute(10)->by(
'guest-ip:' . $request->ip(),
);
}
return Limit::perMinute(30)->by(
'user:' . $user->getAuthIdentifier(),
);
});
認証済みユーザーはユーザーID単位、未認証ユーザーはIPアドレス単位に分ける例です。利用者ごとに制限を分けたい処理では、このような切り替えを検討できます。
ただし、認証済みユーザーだけを信頼すれば十分とは限りません。攻撃者が大量のアカウントを作成できる場合などは、ユーザーID単位の制限に加えて、IPアドレス単位の制限も必要になる可能性があります。
入力値単位で制限する
ログインやOTP発行では、メールアドレスなどの入力値ごとに制限したい場合があります。キャッシュへ生のメールアドレスをキーとして保存しないよう、ここではハッシュ化して使います。
RateLimiter::for('login', function (Request $request): Limit {
$identifier = mb_strtolower(
trim((string) $request->input('email')),
);
$identifierKey = hash('sha256', $identifier);
return Limit::perMinute(5)->by(
'login-email:' . $identifierKey,
);
});
大文字と小文字を同一視するかどうかは、アプリケーションのログイン識別子の仕様に合わせます。メールアドレスを常に小文字へ正規化していないアプリケーションで、レート制限だけ小文字化すると、アプリケーション本体の扱いとずれる可能性があるので注意。
入力値を制限キーに使う場合は、次の点も確認します。
- 入力値が空の場合に、すべての空入力が同じ制限へ入らないか。
- 入力値をそのままキャッシュキーやログへ保存してよいか。
- 大文字・小文字、前後の空白、Unicodeの正規化をどう扱うか。
- 入力値だけで制限せず、IPアドレス単位の制限も必要ではないか。
ログイン処理へ応用する
ログイン処理では、IPアドレス単位と入力された識別子単位の両方を制限する方法があります。片方だけにすると、それぞれ次のような弱点があります。
- IPアドレスだけ: 攻撃者が複数のIPアドレスを使うと、識別子ごとの試行を抑えにくい。
- 識別子だけ: 1つのIPアドレスから大量の識別子を試す攻撃を抑えにくい。
Laravelでは、1つのNamed Rate Limiterから複数のLimitを返せます。次の例では、IPアドレス単位とメールアドレス単位の制限を同時に設定しています。
RateLimiter::for('login', function (Request $request): array {
$email = mb_strtolower(
trim((string) $request->input('email')),
);
$emailKey = hash('sha256', $email);
return [
Limit::perMinute(30)->by(
'login-ip:' . $request->ip(),
),
Limit::perMinute(5)->by(
'login-email:' . $emailKey,
),
];
});
これらの数値は実際の推奨値ではありません。ログイン画面であれば、正しいパスワードを入力した利用者が誤って制限されないか、攻撃を十分に抑えられるかを確認しながら決めます。
ログインルートへ適用します。
Route::post('/login', [LoginController::class, 'store'])
->middleware('throttle:login');
レート制限は、パスワードの照合やアカウントロックの代わりではありません。ログイン処理では、パスワードのハッシュ化、認証失敗時の表示、セッション管理、監視なども別途設計します。
全体上限と識別子ごとの上限を組み合わせる
特定の利用者ごとの制限だけではなく、エンドポイント全体の負荷も抑えたい場合があります。
例えば、全体で1分間に1,000回まで、さらにユーザーまたはIPアドレスごとに1分間に10回まで、という組み合わせです。
RateLimiter::for('repeatable-action', function (Request $request): array {
$identity = $request->user() !== null
? 'user:' . $request->user()->getAuthIdentifier()
: 'ip:' . $request->ip();
return [
// この処理全体に対する上限
Limit::perMinute(1000),
// 利用者またはIPアドレスごとの上限
Limit::perMinute(10)->by('identity-minute:' . $identity),
];
});
全体上限へby()を付けていないため、リミッター全体のカウントになります。個別の制限にはidentity-minute:という接頭辞を付けています。
同じ制限単位へ複数の期間を設定する場合も、キーを分けます。
return [
Limit::perMinute(10)->by('minute:' . $identity),
Limit::perDay(1000)->by('day:' . $identity),
];
接頭辞を付けずに同じキーを使うと、分単位と日単位の制限が意図せず同じカウントへ入る可能性があります。複数の制限を使う場合は、制限の用途や期間をキーへ含めておくと、意図を確認しやすくなります。
429レスポンスをカスタマイズする
制限を超えた場合、Laravelは429 Too Many Requestsレスポンスを返します。APIの場合は、クライアントが扱いやすいJSON形式へ変更することもできます。
RateLimiter::for('repeatable-action', function (Request $request): Limit {
return Limit::perMinute(10)
->by('ip:' . $request->ip())
->response(function (Request $request, array $headers) {
return response()->json([
'message' => 'リクエストが多すぎます。少し待ってから再試行してください。',
], 429, $headers);
});
});
第2引数の$headersには、レート制限に関係するレスポンスヘッダーが渡されます。カスタムレスポンスを返す場合も、このヘッダーを引き継ぐようにします。
HTML画面で使う場合は、JSONではなくリダイレクトやBladeのエラーページを返す構成も考えられます。重要なのは、制限に到達したことを利用者へ伝え、すぐに何度も再送するのではなく、再試行まで待つよう案内することです。
レスポンスの内容に応じてカウントする
通常は、ミドルウェアを通過したリクエストを制限の対象として考えます。一方で、特定のHTTPステータスを返したときだけカウントしたい場合は、after()を使う方法があります。
例えば、存在しないリソースへのアクセスを404のときだけカウントする例です。
use Symfony\Component\HttpFoundation\Response;
RateLimiter::for('resource-not-found', function (Request $request): Limit {
return Limit::perMinute(10)
->by($request->user()?->getAuthIdentifier() ?: $request->ip())
->after(function (Response $response): bool {
return $response->getStatusCode() === 404;
});
});
after()は、バリデーションエラーを制限対象から外したい場合などにも使えます。ただし、どのレスポンスを数えるかによって、攻撃への強さと通常利用のしやすさが変わります。必要性を確認してから使いましょう。
Redisを使う場合
レート制限のカウントはキャッシュへ保存されます。複数のアプリケーションサーバーやコンテナで同じ制限を共有する場合は、各プロセスが別々のローカルキャッシュを使わないようにします。
Redisをアプリケーションのキャッシュドライバーとして使う場合は、bootstrap/app.phpでthrottleWithRedis()を設定できます。
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__ . '/../routes/web.php',
api: __DIR__ . '/../routes/api.php',
commands: __DIR__ . '/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->throttleWithRedis();
})
->create();
この設定だけでRedis接続が用意されるわけではありません。環境変数やLaravelのキャッシュ設定で、アプリケーションがRedisを使える状態になっていることを先に確認します。
単一サーバーのローカル環境では、ファイルやデータベースのキャッシュでも動作を確認できます。本番環境で複数のサーバーから同じ制限を適用する場合は、カウントを共有できる構成を選びます。
動作を確認する
まず、ルートへNamed Rate Limiterが付いていることを確認します。
php artisan route:list -v
出力されたルートのMiddleware欄に、throttle:repeatable-actionなどの指定が表示されることを確認します。
APIルートへリクエストを送り、制限回数以内では処理が実行され、上限を超えると429になることを確認します。例えば、Feature Testでは次のような流れになります。
public function test_it_rejects_requests_after_the_rate_limit(): void
{
for ($i = 0; $i < 10; $i++) {
$this->postJson('/api/repeatable-action')
->assertStatus(200);
}
$this->postJson('/api/repeatable-action')
->assertStatus(429);
}
テストで使う制限値は、確認しやすい小さな値へ分けても構いません。テスト間でレート制限のカウントが残る場合は、テスト用のキャッシュストアを使い、テストごとに状態を分離します。
キーの分離も確認します。例えば、ユーザーID単位の制限であれば、同じユーザーからの連続リクエストは制限されても、別ユーザーのリクエストまで同じ制限に入らないことを確認します。
IPアドレス単位の制限をテストする場合は、実際のアプリケーションがプロキシをどう扱うかによって、テスト方法が変わります。開発環境で取得されるIPアドレスと、本番環境で取得されるIPアドレスが同じ前提になっているかを確認します。
注意点
- 制限値は一般的な推奨値ではなく、処理の負荷や利用状況を確認したうえで決める。
- IPアドレスだけで制限すると、共有ネットワークの利用者を巻き込む可能性がある。
- ユーザーIDだけで制限すると、未認証状態の攻撃や大量アカウント作成に弱くなる場合がある。
- 入力値をキーに使う場合は、生のメールアドレスなどをキャッシュやログへ残さない。
- プロキシやロードバランサー経由のIPアドレスを、無条件に信頼しない。
- レート制限は認証、認可、アカウントロック、キュー制御の代わりにはならない。
- 429を返した後に、クライアントが自動再試行を繰り返さない設計にする。
- 複数サーバーで同じ制限を適用する場合は、カウントを共有するキャッシュを使う。
- HTMLのPOSTフォームへ適用する場合は、レート制限とは別にCSRF対策も必要になる。
まとめ
LaravelのNamed Rate Limiterを使うと、処理ごとに名前を付けたレート制限を定義し、ルートやルートグループへ適用できます。
RateLimiter::for()でLimit::perMinute()などの制限を定義し、by()でIPアドレス、ユーザーID、入力値などの制限単位を指定します。
ログインのような処理では、IPアドレス単位と入力値単位の制限を組み合わせる方法があります。全体の上限と識別子ごとの上限を併記する場合は、制限キーが衝突しないよう、用途や期間を表す接頭辞も付けておきます。
レート制限の目的は、正当な利用者を機械的に締め出すことではありません。無制限に繰り返されたくない処理を特定し、通常利用を妨げない範囲で、負荷と不正利用のリスクを抑えるように設定しましょう。
参考資料
奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。
これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。
【制作会社・企業様向けサポート】
Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。
ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。
Laravel SocialiteでGoogleログインを実装する|OAuth設定から初回登録まで
2026.09.18
Laravel Socialiteを使ったGoogleログインを、OAuthクライアントの設定からコールバック、Laravelユーザーの登録・ログインまで解説します。Googleアカウントは変更されるメールアドレスではなくsubで識別し、初回登録時は検証済みメールアドレスを確認します。既存アカウントへ自動連携しない設計や、セッション管理、Socialite Fakeで確認するテスト項目も紹介します。
Laravel Pintの使い方:PHPコードの書式を整えてCIで検査する
2026.09.15
Laravel Pintを使ってPHPコードの書式を整える方法を紹介します。ローカルでの自動修正、CIで書式違反だけを検査する--test、対象範囲の指定、pint.jsonの設定、Bladeファイルを扱う際の注意点を整理し、Pintを導入する判断基準も説明します。
GitHub ActionsでPHP・LaravelのCIを始める:テスト・Lint・ビルドを自動化するWorkflowの基本
2026.09.13
PHP・Laravelプロジェクトを対象に、GitHub ActionsのWorkflow、イベント、ジョブ、ステップの基本を説明します。Composerのテスト、npm ci、フロントエンドビルド、Laravel PintをCIへ組み込む方法に加え、featureブランチやPull Requestで実行される条件、失敗時のログ確認、Secrets・権限・Actionのバージョン管理についても整理します。
Laravel 13でArtisanコマンドを作成する:引数・オプション・dry-runの基本
2026.09.10
Laravel 13でArtisanコマンドを作成する方法を、引数・オプション・コンソール出力・終了コードの基本から解説します。日時指定した記事を定期的に公開するDB更新処理を例に、--dry-runで対象を確認してから安全に実行する方法や、スケジューラー・cronでの定期実行についても紹介します。