404 Not Found

404 Not Found


nginx

Laravelミドルウェアの徹底解説

ミドルウェアはLaravelの「セキュリティチェックポイントパイプライン」です。各リクエストは荷物のように層ごとのチェックを通過し, 不合格のものは即座に傍受され, 合格したものは次のステージに進みます。

1. 学ぶこと


2. アーキテクトの本当の話

(1) 痛点:すべてのセキュリティチェックがコントローラに集中している

Bobは各コントローラメソッドの先頭に10行のバリデーションコードを書いていました。認証チェック, テナント分離, レート制限, CORS, ログ。50メソッド x 10行 = 500行の重複コード。Aliceが新しいセキュリティポリシーを追加する際, 50箇所を変更する必要があり, 3箇所の見落としがセキュリティ脆弱性につながりました。Charlieは「ミドルウェアって聞いたことない?」と言いました。

(2) ミドルウェアの解決策

ミドルウェアは共通のバリデーションロジックを個別のクラスに抽出し, 各リクエストが自動的に複数層のミドルウェアを通過します。認証, レート制限, CORS, テナント分離を経て, コントローラはビジネスロジックに専念します。

PHP
// 変更前 — 各メソッドに10行のチェック
public function index() {
    if (!auth()->check()) abort(401);
    if (!tenant()->isActive()) abort(403);
    if (RateLimiter::tooManyAttempts(...)) abort(429);
    // ... ようやくビジネスロジック
}

// 変更後 — ミドルウェアがすべてのチェックを処理
Route::middleware(['auth', 'tenant.resolve', 'throttle:60,1'])
    ->get('/shops', [ShopController::class, 'index']);
// コントローラにはビジネスロジックのみ

(3) 成果

Bobがミドルウェアを実装した後, コントローラのコードが60%削減されました。Aliceの新しいセキュリティポリシーは1つのミドルウェアを変更するだけで済み, 見落としはゼロになりました。


3. ミドルウェアパイプラインの実行フロー

(1) リクエストがミドルウェアを通過する全体プロセス

100%
flowchart LR
    A[リクエスト] --> B[グローバルMW 1]
    B --> C[グローバルMW 2]
    C --> D[ルートMW 1]
    D --> E[ルートMW 2]
    E --> F[コントローラ]
    F --> G[レスポンス]
    G --> H[後MW 2]
    H --> I[後MW 1]
    I --> J[クライアント]

(2) オニオンモデル

TEXT
リクエスト →
  ミドルウェア A (before) →
    ミドルウェア B (before) →
      コントローラ → レスポンス
    ミドルウェア B (after) →
  ミドルウェア A (after) →
レスポンス

各ミドルウェアは以下のことが可能です:

(1) ▶ サンプル:ミドルウェアのオニオンモデルを理解する

PHP
// app/Http/Middleware/LogRequests.php
class LogRequests
{
    public function handle(Request $request, Closure $next): Response
    {
        // BEFORE — 受信リクエストをログ
        Log::info('Request:', [
            'method' => $request->method(),
            'url' => $request->fullUrl(),
            'ip' => $request->ip(),
        ]);

        // 次のミドルウェアに渡す
        $response = $next($request);

        // AFTER — レスポンスステータスをログ
        Log::info('Response:', [
            'status' => $response->getStatusCode(),
            'duration' => defined('LARAVEL_START') ? round((microtime(true) - LARAVEL_START) * 1000) : null,
        ]);

        return $response;
    }
}

出力:

TEXT
// 実行成功

4. 3つの登録方法

(1) グローバルミドルウェア

すべてのリクエストで実行されます。ルートでの指定は不要です。

PHP
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->append([
        \App\Http\Middleware\LogRequests::class,
        \App\Http\Middleware\SetLocale::class,
    ]);

    $middleware->remove([
        \Illuminate\Foundation\Http\Middleware\TrimStrings::class,
    ]);
})

(2) ルートミドルウェア

ルート定義で指定するミドルウェアです。

PHP
// エイリアスの登録
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'tenant' => \App\Http\Middleware\TenantResolve::class,
        'role' => \App\Http\Middleware\CheckRole::class,
        'ability' => \App\Http\Middleware\CheckAbility::class,
    ]);
})

// ルートでの使用
Route::middleware(['auth', 'tenant', 'role:admin'])
    ->get('/admin/users', [AdminController::class, 'users']);

(3) ミドルウェアグループ

ミドルウェアのセットをパッケージ化し, グループとして適用します。

グループ名 デフォルトの内容 適用先
web StartSession, EncryptCookies, VerifyCsrfToken Webルーティング
api Throttle:api, SubstitueBindings APIルーティング
PHP
// ミドルウェアグループのカスタマイズ
->withMiddleware(function (Middleware $middleware) {
    $middleware->appendToGroup('api', [
        \App\Http\Middleware\TenantResolve::class,
    ]);

    $middleware->prependToGroup('web', [
        \App\Http\Middleware\SetLocale::class,
    ]);
})
登録方法 タイミング 適用場面
グローバル すべてのリクエスト ログ, CORS
ルートエイリアス 指定ルート 認証, テナント
ミドルウェアグループ グループ内ルート Web/APIグループ

(1) ▶ サンプル:ShopMetricsミドルウェア登録

PHP
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    // グローバルミドルウェア
    $middleware->append([
        \App\Http\Middleware\SetLocale::class,
    ]);

    // エイリアス
    $middleware->alias([
        'tenant' => \App\Http\Middleware\TenantResolve::class,
        'role' => \App\Http\Middleware\CheckRole::class,
        'ability' => \App\Http\Middleware\CheckAbility::class,
    ]);

    // APIグループに追加
    $middleware->appendToGroup('api', [
        \App\Http\Middleware\EnsureJsonAccept::class,
    ]);
})

出力:

TEXT
// 実行成功

5. カスタムミドルウェア

(1) ミドルウェアの作成

BASH
php artisan make:middleware TenantResolve
php artisan make:middleware CheckRole
php artisan make:middleware EnsureJsonAccept

(2) TenantResolveミドルウェア

PHP
// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();
        if (!$user || !$user->tenant_id) {
            abort(403, 'No tenant associated with this account.');
        }

        $tenant = $user->tenant;

        if ($tenant->status !== 'active') {
            abort(403, 'Tenant account is ' . $tenant->status . '.');
        }

        // グローバルスコープ用にテナントをコンテナに保存
        app()->instance(Tenant::class, $tenant);

        // リクエストにテナントコンテキストを設定
        $request->attributes->set('tenant', $tenant);

        return $next($request);
    }
}

(3) CORSミドルウェア

PHP
// app/Http/Middleware/HandleCors.php
class HandleCors
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        $response->headers->set('Access-Control-Allow-Origin', config('cors.allowed_origins'));
        $response->headers->set('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
        $response->headers->set('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Tenant-Id');
        $response->headers->set('Access-Control-Max-Age', '86400');

        if ($request->isMethod('OPTIONS')) {
            $response->setStatusCode(204);
        }

        return $response;
    }
}

(1) ▶ サンプル:ShopMetricsレート制限ミドルウェア

PHP
// app/Http/Middleware/PerTenantRateLimit.php
class PerTenantRateLimit
{
    public function handle(Request $request, Closure $next, int $maxAttempts = 60, int $decayMinutes = 1): Response
    {
        $tenant = app()->make(Tenant::class);
        $key = 'tenant:' . $tenant->id . ':' . $request->ip();

        if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
            $seconds = RateLimiter::availableIn($key);
            abort(429, "Too many requests. Try again in {$seconds} seconds.");
        }

        RateLimiter::hit($key, $decayMinutes * 60);

        $response = $next($request);

        $response->headers->set('X-RateLimit-Limit', $maxAttempts);
        $response->headers->set('X-RateLimit-Remaining', $maxAttempts - RateLimiter::attempts($key));

        return $response;
    }
}

出力:

TEXT
// 実行成功

6. ミドルウェアパラメータ

(1) パラメータの渡し方

PHP
// パラメータ付きルート定義
Route::middleware('role:admin,super_admin')
    ->get('/admin/dashboard', [AdminController::class, 'dashboard']);

// ミドルウェアでパラメータを受け取る
class CheckRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (!in_array($request->user()->role, $roles)) {
            abort(403, 'Insufficient role. Required: ' . implode(', ', $roles));
        }
        return $next($request);
    }
}

(2) throttleパラメータ

PHP
// パラメータ付きビルトインthrottle
Route::middleware('throttle:60,1')->group(function () {
    // 1分間に60リクエスト
});

Route::middleware('throttle:10,1')->group(function () {
    // 1分間に10リクエスト (重いエンドポイント用)
});

// カスタムレートリミッター
RateLimiter::for('api', function (Request $request) {
    return $request->user()
        ? Limit::perMinute(60)->by($request->user()->id)
        : Limit::perMinute(10)->by($request->ip());
});

RateLimiter::for('tenant-api', function (Request $request) {
    $tenant = app()->make(Tenant::class);
    $plan = $tenant->subscription?->plan;

    return Limit::perMinute(match ($plan->slug ?? 'starter') {
        'enterprise' => 300,
        'pro' => 120,
        default => 60,
    })->by($tenant->id);
});
パラメータフォーマット 説明
middleware:p1 単一パラメータ role:admin
middleware:p1,p2 複数パラメータ role:admin,editor
throttle:max,decay スロットリングパラメータ throttle:60,1

(1) ▶ サンプル:ShopMetricsサブスクリプションベースのレート制限

PHP
// app/Providers/AppServiceProvider.php
public function boot(): void
{
    RateLimiter::for('tenant-api', function (Request $request) {
        $tenant = app()->make(Tenant::class);
        $plan = $tenant->plan ?? null;

        return Limit::perMinute(match ($plan->slug ?? 'starter') {
            'enterprise' => 600,
            'pro' => 120,
            default => 60,
        })->by($tenant->id)->response(function () {
            return response()->json([
                'message' => 'Rate limit exceeded. Upgrade your plan for higher limits.',
                'upgrade_url' => route('pricing'),
            ], 429);
        });
    });
}

// routes/api.php
Route::middleware(['auth:sanctum', 'tenant', 'throttle:tenant-api'])
    ->prefix('v1')
    ->group(function () {
        Route::apiResource('shops', Api\ShopController::class);
    });

出力:

TEXT
// 実行成功

7. ミドルウェアの順序と優先度

(1) デフォルトの優先度

ミドルウェアは登録された順序で実行されますが, 特定のミドルウェアは先に実行される必要があります (例:CORSは認証の前に処理される必要があります)。

PHP
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->priority([
        \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TenantResolve::class,  // CORSの後に実行する必要がある
        \App\Http\Middleware\Authenticate::class,
    ]);
})

(2) 終了可能ミドルウェア

PHP
// app/Http/Middleware/SendAnalyticsEvent.php
class SendAnalyticsEvent
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    // レスポンスがクライアントに送信された後に実行
    public function terminate(Request $request, Response $response): void
    {
        // 非ブロッキングの分析ログ
        AnalyticsEvent::create([
            'tenant_id' => app()->make(Tenant::class)?->id,
            'path' => $request->path(),
            'method' => $request->method(),
            'status' => $response->getStatusCode(),
            'duration_ms' => defined('LARAVEL_START')
                ? round((microtime(true) - LARAVEL_START) * 1000)
                : null,
        ]);
    }
}
ライフサイクルメソッド タイミング 目的
handle() (before) リクエスト受信時 認証, フィルタ, リクエストの変更
handle() (after) レスポンス返却時 レスポンスヘッダーの変更
terminate() レスポンス送信後 ログ, 統計, クリーンアップ

(1) ▶ サンプル:ShopMetricsミドルウェア優先度設定

PHP
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        // 優先度 — CORSが先, 認証がテナントの前
        $middleware->priority([
            \Illuminate\Http\Middleware\HandleCors::class,
            \App\Http\Middleware\TenantResolve::class,
            \App\Http\Middleware\Authenticate::class,
            \App\Http\Middleware\CheckRole::class,
        ]);

        // エイリアス
        $middleware->alias([
            'tenant' => \App\Http\Middleware\TenantResolve::class,
            'role' => \App\Http\Middleware\CheckRole::class,
            'tenant.throttle' => \App\Http\Middleware\PerTenantRateLimit::class,
        ]);

        // APIグループに追加
        $middleware->appendToGroup('api', [
            \App\Http\Middleware\EnsureJsonAccept::class,
        ]);
    });

出力:

TEXT
// 実行成功

8. 総合例:ShopMetricsミドルウェアアーキテクチャ

PHP
// ============================================
// 総合例: ShopMetricsミドルウェアスタック
// 対象: グローバル, ルート, グループ, パラメータ, 優先度, terminable
// ============================================

// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();
        if (!$user?->tenant_id) {
            abort(403, 'No tenant associated.');
        }
        $tenant = $user->tenant;
        if ($tenant->status !== 'active') {
            abort(403, 'Tenant is ' . $tenant->status);
        }
        app()->instance(Tenant::class, $tenant);
        return $next($request);
    }
}

// app/Http/Middleware/CheckRole.php
class CheckRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (!in_array($request->user()?->role, $roles)) {
            abort(403, 'Required role: ' . implode('|', $roles));
        }
        return $next($request);
    }
}

// app/Http/Middleware/EnsureSubscriptionActive.php
class EnsureSubscriptionActive
{
    public function handle(Request $request, Closure $next): Response
    {
        $tenant = app()->make(Tenant::class);
        $subscription = $tenant->subscription;
        if (!$subscription || $subscription->status !== 'active') {
            abort(402, 'Active subscription required.');
        }
        return $next($request);
    }
}

// app/Http/Middleware/LogApiRequests.php
class LogApiRequests
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    public function terminate(Request $request, Response $response): void
    {
        ApiLog::create([
            'tenant_id' => app()->make(Tenant::class)?->id,
            'user_id' => $request->user()?->id,
            'method' => $request->method(),
            'path' => $request->path(),
            'status' => $response->getStatusCode(),
            'ip' => $request->ip(),
        ]);
    }
}

// 完全なミドルウェアスタックを使用するルート
Route::middleware(['auth:sanctum', 'tenant', 'throttle:tenant-api', 'subscription.active'])
    ->prefix('v1')->group(function () {
        Route::apiResource('shops', ShopController::class);
        Route::middleware('role:tenant_owner,analyst')
            ->apiResource('analytics', AnalyticsController::class)->only(['index', 'show']);
        Route::middleware('role:tenant_owner')
            ->apiResource('settings', SettingsController::class);
    });

❓ よくある質問

Q ルートミドルウェアとコントローラコンストラクタミドルウェアの違いは何ですか?
A ルートミドルウェアはルートが一致した時に適用され, より柔軟です (ルートごとにカスタマイズ可能)。コントローラコンストラクタミドルウェアはコントローラがインスタンス化される時に実行され, コントローラ内のすべてのメソッドに適しています。可能な限りルートミドルウェアを使用してください。
Q terminateメソッドはいつ実行されますか?
A レスポンスがクライアントに送信された後に実行されます (register_shutdown_functionに似ています)。レスポンスに影響を与える必要のないログや統計などの操作に適しています。terminateはユーザーをブロックしません。
Q ミドルウェアの順序が間違っているとどのような問題が発生しますか?
A CORSミドルウェアは認証の前に実行する必要があります。そうしないと, プレフライトリクエスト (OPTIONS)が認証ミドルウェアに傍受され, 401エラーが返されます。TenantResolveはビジネスミドルウェアの前に実行する必要があります。そうしないとtenant()がnullになります。
Q ミドルウェアでリクエストをショートサーキットするにはどうしますか?
A $next($request)を呼ばずにResponseオブジェクトを直接返します:return response()->json(['error' => 'Unauthorized'], 401);。後続のミドルウェアとコントローラは実行されません。
Q グローバルミドルウェアで特定のルートを除外できますか?
A Laravel 11では, $middleware->skipWhen(callback)を使用するか, ミドルウェア内で$request->is('api/*')を使ってルートをスキップするかを判断できます。グローバルミドルウェアにルートチェックを含めることは推奨されません。
Q RateLimiterとthrottleミドルウェアの関係は何ですか?
A throttleミドルウェアは内部的にRateLimiterファサードを使用します。カスタムレート制限ロジックを作成する際は, まずRateLimiter::for()でストラテジーを定義し, ルートでthrottle:ストラテジー名として参照します。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐):TenantResolveミドルウェアを作成し, 認証済みユーザーからテナントを解決してコンテナに保存し, APIルートグループで使用してください。テナントがバインドされていないユーザーがAPIにアクセスしようとすると403エラーが返されることをテストしてください。

  2. 応用問題 (⭐⭐):PerTenantRateLimitミドルウェアを実装し, テナントIDとIPアドレスに基づいてレート制限を適用し, サブスクリプションプランに応じてレート制限を動的に設定してください (Starter:60/分, Pro:120/分, Enterprise:600/分)。

  3. チャレンジ (⭐⭐⭐):完全なミドルウェア優先度システムを設計してください。CORS → TenantResolve → Auth → SubscriptionActive → CheckRole → throttleの順序で, 各ミドルウェアが正しいタイミングで実行されることを確認します。OPTIONSプレフライトリクエストが認証に傍受されないことをテストで検証してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%