Laravelのカスタムミドルウェア作成と活用法:認証以外の横断処理を安全に差し込む

Laravelのカスタムミドルウェア作成と活用法:認証以外の横断処理を安全に差し込む

ミドルウェアは、コントローラに入る前/レスポンスを返す前後に「横断的な処理」を挟む仕組み。認証・CSRFだけでなく、IP制限、メンテナンス制御、リクエストID付与、APIキー検証、利用規約同意チェック、ロール制御、ヘッダ付与、レート制限などに使える。この記事は、カスタムミドルウェアの作り方から、ルート/グループへの適用、引数付きミドルウェア、デバッグ、よくあるエラー条件までを実務向けに整理する。

ミドルウェアが効く場面:コントローラに書くと増殖する処理を1箇所に寄せる

同じチェックが複数コントローラに散ると、仕様変更で漏れが出る。ミドルウェアにすると「入口で必ず通る」形になり、適用範囲(全体/特定ルート/特定グループ)で制御できる。
典型例:APIキー必須、特定IPだけ許可、管理画面のみ追加ヘッダ、リクエストログ、利用規約同意の強制など。

作成手順:make:middleware で雛形を作る

まずは雛形を作り、handle() で通す/止めるを決める。

php artisan make:middleware EnsureApiKey

// app/Http/Middleware/EnsureApiKey.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureApiKey
{
    public function handle(Request $request, Closure $next): Response
    {
        // ここでチェックして通す/止める
        return $next($request);
    }
}

サンプル:APIキー必須ミドルウェア(不足時は401)

ヘッダ X-API-KEY を必須にし、値が一致しない場合は拒否する例。エラー条件が明確なので、実務で使い回しやすい。

// app/Http/Middleware/EnsureApiKey.php(例)
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureApiKey
{
    public function handle(Request $request, Closure $next): Response
    {
        $provided = $request->header('X-API-KEY');               // エラー条件①:ヘッダ未設定でnull
        $expected = config('app.internal_api_key');              // エラー条件②:設定未投入でnull

        if (!$expected) {
            // 設定ミスは運用事故なので 500 に寄せて気づけるようにする
            return response()->json([
                'message' => 'Server misconfigured: API key not set.'
            ], 500);
        }

        if (!$provided || !hash_equals($expected, $provided)) {  // エラー条件③:不一致
            return response()->json([
                'message' => 'Unauthorized.'
            ], 401);
        }

        return $next($request);
    }
}

登録と適用:ルート/グループに付けて範囲を固定する

ミドルウェアは「どこに適用するか」が一番重要。全体に入れると影響範囲が広すぎることがあるので、まずはルートグループ単位が安全。

// routes/api.php(例)
use Illuminate\Support\Facades\Route;

Route::middleware(['ensure.api.key'])->group(function () {
    Route::get('/internal/health', fn () => ['ok' => true]);
    Route::post('/internal/sync', fn () => ['queued' => true]);
});

※エイリアス(ensure.api.key)の登録方法はプロジェクトのLaravelバージョン構成に依存するため、ミドルウェアの登録箇所(Kernel/Bootstrap/Providerなど)に合わせて1箇所で統一するのが前提。

引数付きミドルウェア:同じ処理を条件だけ変えて使い回す

IP制限やロール制限などは、引数で動きを変えると便利。例として「特定ロールだけ許可」の形。

// handle の第3引数以降に引数を受けられる例
public function handle(Request $request, Closure $next, string $role): Response
{
    $user = $request->user();                 // エラー条件①:未ログインだとnull
    if (!$user) {
        return redirect()->route('login');    // もしくは 401
    }

    if ($user->role !== $role) {              // エラー条件②:権限不足
        abort(403);
    }

    return $next($request);
}

前処理/後処理:レスポンスにヘッダを足す、実行時間を測る

ミドルウェアは「レスポンスを返す前後」で処理できる。入口で時間計測→出口でログ化、レスポンスヘッダ追加などが定番。

public function handle(Request $request, Closure $next): Response
{
    $start = microtime(true);

    $response = $next($request);

    $elapsedMs = (microtime(true) - $start) * 1000;

    $response->headers->set('X-Request-Id', (string) $request->header('X-Request-Id', uniqid()));
    $response->headers->set('X-Elapsed-Ms', (string) (int) $elapsedMs);

    logger()->info('request.done', [
        'path' => $request->path(),
        'elapsed_ms' => (int) $elapsedMs,
    ]);

    return $response;
}

よくあるエラー条件:動かない/効かない/無限リダイレクト

ミドルウェア周りで詰まりやすいポイントと発生条件。
・ルートに付けたのに効かない:登録(エイリアス/クラス指定)ができていない、適用グループが違う
・401/403が連発:認証ミドルウェアより先に権限チェックしている(順序問題)
・無限リダイレクト:未ログイン→loginへ→loginにも同じミドルウェアが掛かっている
・CORS/プリフライトが落ちる:OPTIONSを弾いている(APIキー強制など)
・500になる:config値が未設定、env名のtypo、キャッシュが古い(config:cache)
ミドルウェアの“順番”と“適用範囲”が原因の大半を占める。

デバッグ手順:ミドルウェア内ログ→ルート単位で切り分ける

動作確認は「当たっているか」と「条件分岐がどこで落ちたか」を分ける。
・handle() の最初にログを1行入れる(本番は抑制)
・ヘッダやユーザーID、ルート名をログ化
・まず単一ルートにだけ適用→問題なければグループへ拡張
ミドルウェアの不具合は影響範囲が広いので、適用範囲は段階的に広げるのが安全。

まとめ:カスタムミドルウェアを安全に運用する要点

・横断処理(認証以外の共通チェック)を入口に集約できる
・まずはルートグループ単位で適用し、影響範囲を管理する
・重い処理は入れすぎない(ログ/外部APIは慎重に)
・引数付きで再利用し、分岐条件を明確にする
・エラー条件(未ログイン、ヘッダ不足、権限不足、設定ミス)をレスポンスとログで区別する
・順序問題(authより先に権限チェック等)と無限リダイレクトを最優先で潰す