【Laravel】Middlewareの自作方法|認証・ロギング・APIキー検証の実装パターン

はじめに

Laravel のルート処理には、認証チェックやログ出力、API キー検証など、多くの HTTP リクエストで共通する処理があります。これらをコントローラやトレイトに散らすと重複が増え、適用順の取り違えも起きやすくなります。ミドルウェアは、リクエストとレスポンスのライフサイクルに挟み込んで共通処理をまとめるための仕組みです。

「API ルートだけトークン認証を通したい」「全リクエストにリクエスト ID をログへ残したい」「外部連携用の API キーをヘッダで検証したい」といった要件は、実務でよく出てきます。各コントローラメソッドの先頭に同じ処理を書くと、テストや順序変更のたびに散らばったコードを追うことになります。

ミドルウェアでは、handle メソッドでリクエスト前後の処理をまとめ、ルート単位・ミドルウェアグループ単位・グローバル単位で適用順を制御できます。認証・ロギング・API キー検証は、この適用順と組み合わせて設計するのが一般的です。

細かな API は Laravel のメジャーバージョンで変わることがあります。手元の composer.jsonlaravel/framework に合わせ、Middleware の公式ドキュメントの該当バージョンを開いて照合すると再現しやすいです。

検証環境の前提: コード例は Laravel 11 以降の構成を主な前提としています。ミドルウェア登録は bootstrap/app.phpwithMiddleware を想定しています。Laravel 10 以前では app/Http/Kernel.php$middlewareAliases / $middlewareGroups に相当する場所へ登録します。

この記事では次の範囲を整理します。

  • ミドルウェアの作成と登録の基本
  • 認証・ロギング・API キー検証を題材にした実装パターン
  • 3 つを組み合わせたルート設計の例
  • 実装時の注意点とよくある失敗例

背景:ミドルウェアの実行順とグループ

Laravel では、リクエストがルートに到達するまでと、その後のレスポンス生成までに複数のミドルウェアが積まれます。Middleware の説明にあるとおり、handle($request, Closure $next)$next を呼ぶ前後で前処理・後処理を書けます。

適用のしかた用途のイメージ
グローバルアプリ全体に共通する処理(少数・慎重に)
web / api などのグループ既定セットをベースに、グループ単位で挙動を変える
ルート単位特定エンドポイントだけに認証や API キー検証

routes/api.php に定義したルートは、通常 api ミドルウェアグループを経由します。bootstrap/app.php でグループ既定を把握し、routes/api.php 側では追加で必要なミドルウェアだけを付けると、新規ルート追加時の読み返しが楽になります。

ルートに複数ミドルウェアを指定した場合、配列の左から順に前処理が走ります。ログの後処理(処理時間の記録など)は、レスポンスが返る $next($request) のあとに実行されます。

Route::middleware(['api.key', 'auth:sanctum', 'log.incoming'])->group(function () {
    Route::get('/integrations/me', [IntegrationController::class, 'me']);
});

上記では、左から API キー検証 → 認証 → ログ(入口記録と完了ログ)の順になります。

実装と検証

ミドルウェアの生成と最小実装

Artisan でクラスを生成します。

php artisan make:middleware LogIncomingRequest

入口だけを記録する最小例です。実務では後述の AssignRequestId に置き換えるか、併用して段階的に広げることが多いです。

<?php

namespace App\Http\Middleware;

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

class LogIncomingRequest
{
    public function handle(Request $request, Closure $next): Response
    {
        logger()->info('incoming', [
            'method' => $request->method(),
            'path' => $request->path(),
        ]);

        return $next($request);
    }
}

レスポンスが返るまで計測したい場合は、$next($request) のあとでレスポンスを受け取り、終了処理を書けます。本記事の計測例は handle 内後処理で足りるケースが多いです。レスポンス送信後にまとめて処理したい場合は Terminable Middlewareterminate を検討します。

登録とルートへの適用

Laravel 11 以降では bootstrap/app.phpwithMiddleware でエイリアス付けやグループへの追加をします(Registering Middleware)。

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'log.incoming' => \App\Http\Middleware\LogIncomingRequest::class,
    ]);
})

まずは 1 ルートだけに付けて動作確認します。

Route::middleware(['log.incoming'])->group(function () {
    Route::get('/health', fn () => ['status' => 'ok']);
});

問題なければ appendToGroupapi グループ全体へ広げます。

$middleware->appendToGroup('api', [
    \App\Http\Middleware\LogIncomingRequest::class,
]);

認証パターン

ユーザー認証が必要な API では、組み込みの認証ミドルウェアをルートに付けるのが一般的です(Protecting Routes)。以下は Sanctum がセットアップ済みのプロジェクトを想定しています。未導入の場合は Laravel Sanctum のインストール手順を先に確認してください。

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/me', fn () => auth()->user());
});

認証ミドルウェアは「ログイン済みか」を判定する層です。操作権限(更新・削除の可否など)は Policy や Form Request の authorize に任せ、ミドルウェア側では認証の有無までに留めると責務が分かれやすくなります。

ロギングパターン

ログ用途では次を切り分けると運用しやすくなります。

  • 前処理: リクエスト ID の発行、開始時刻の記録
  • 後処理: ステータスコード、処理時間(ミリ秒)の記録

リクエスト ID を付与する例です。$request->attributes はリクエスト処理中だけ使う内部メタデータで、入力値やヘッダとは別枠です。後続のミドルウェアやコントローラからは $request->attributes->get('request_id') で参照できます。

<?php

namespace App\Http\Middleware;

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

class AssignRequestId
{
    public function handle(Request $request, Closure $next): Response
    {
        $requestId = (string) Str::uuid();
        $request->attributes->set('request_id', $requestId);

        $startedAt = microtime(true);
        $response = $next($request);

        logger()->info('request.completed', [
            'request_id' => $requestId,
            'status' => $response->getStatusCode(),
            'duration_ms' => (int) ((microtime(true) - $startedAt) * 1000),
        ]);

        return $response;
    }
}

個人情報や認証情報がログに落ちないよう、ログ出力する項目はホワイトリスト的に限定する運用が安全側です。Authorization ヘッダや API キー本体はログに含めないでください。

API キー検証パターン

外部サービス向けの内部 API など、ユーザー認証ではなく固定の API キーで保護したい場合は、専用ミドルウェアを用意します。

php artisan make:middleware ValidateApiKey

キーは .env に置き、config/services.php 経由で参照します。

// config/services.php(抜粋)
'internal_api' => [
    'key' => env('INTERNAL_API_KEY'),
],
# .env
INTERNAL_API_KEY=your-test-key

ミドルウェア本体では、ヘッダ値と設定値を hash_equals で照合します。固定長の API キー照合では、比較時間が文字列内容に依存しにくい hash_equals を使う慣習が多いです。

<?php

namespace App\Http\Middleware;

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

class ValidateApiKey
{
    public function handle(Request $request, Closure $next): Response
    {
        $provided = $request->header('X-Api-Key');
        $expected = config('services.internal_api.key');

        if ($provided === null || $expected === null) {
            return response()->json(['message' => 'Unauthorized.'], 401);
        }

        if (! hash_equals($expected, $provided)) {
            return response()->json(['message' => 'Unauthorized.'], 401);
        }

        return $next($request);
    }
}

避けるべき実装: ?api_key=... のようにクエリ文字列へ API キーを載せると、アクセスログや Referer 経由でキーが漏れやすくなります。ヘッダ(X-Api-Key など)で渡し、ミドルウェア側でもヘッダだけを参照するように統一してください。

観点実装のポイント
キーの保管.env に置き、config/services.php 経由で参照する
ヘッダ名チーム内で X-Api-Key などに統一し、ドキュメント化する
失敗時の応答API ルートでは JSON と 401 を返し、Web ルートと混在させない

本記事では単一キーの照合に限定します。複数連携先ごとにキーを分けたい場合は、設定を配列化し、Middleware Parameters でルートごとにキーセットを渡す形へ拡張できます。

3 パターンを組み合わせる統合例

実装したミドルウェアを bootstrap/app.php でエイリアス登録します。

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'log.incoming' => \App\Http\Middleware\LogIncomingRequest::class,
        'api.key' => \App\Http\Middleware\ValidateApiKey::class,
    ]);
})

ルート側では、左から API キー → 認証 → ログの順で指定します。

Route::middleware(['api.key', 'auth:sanctum', 'log.incoming'])->group(function () {
    Route::get('/integrations/me', [IntegrationController::class, 'me']);
});

Route::middleware([...]) を見れば、このエンドポイントは API キー必須・認証必須・ログ付きと一目で分かります。

実装時の注意点

実行順: 認証より前の段階でログインユーザー名をログへ出そうとすると、値は期待どおりになりにくいです。API キー検証 → 認証 → ロギング(後処理)のように、依存関係を整理して並べます。

副作用の範囲: グローバルに登録したミドルウェアは想定外のルートにも効きます。API キー検証は必要なルートグループに限定するのが無難です。

レスポンス後の処理: ストリーミングレスポンスでは duration_ms が期待とずれることがあります。計測要件がある場合は、対象レスポンス種別を確認します(Terminable Middleware)。

設定とコードの二重管理: API キーやログ項目をコードと設定ファイルの両方に散らすと、環境差分の把握が難しくなります。キーは設定ファイル、判定ロジックはミドルウェア、と役割を分けておくとよいです。

失敗例:API キーをクエリパラメータで受け取っていた

?api_key=... のようにクエリ文字列へ API キーを載せると、プロキシや CDN のログ設定によっては URL がそのまま記録され、キーが漏れやすくなります。既存クライアントがクエリ方式の場合は、移行期間を設けてヘッダ方式へ切り替えるのが現実的です。

学び:ミドルウェアは HTTP の共通ポリシーをコードとして固定する場所

認証・ロギング・API キー検証は、要件としては別々でも、HTTP の入口では組み合わせて現れます。Route::middleware([...]) にポリシーを宣言的に書くことで、レビュー時に「この API は何を要求するか」をコードだけで追えるようになります。

まとめ

  • ミドルウェアは handle を中心に、リクエスト前後の共通処理をまとめる仕組みである。
  • 認証は組み込みミドルウェア、ロギングは前後処理の切り分け、API キー検証はヘッダ照合と hash_equals が実務でよく使われるパターンである。
  • 新規ルート追加時は Route::middleware を見れば認証要否が分かり、ログ調査は request_id で横断できる。

次に試せること

  1. API キー検証を 1 ルートで試す
    • .envINTERNAL_API_KEY を追加する
    • config/services.phpinternal_api.key を追加する
    • make:middleware ValidateApiKey で実装し、bootstrap/app.phpapi.key エイリアスを登録する
    • ルートに middleware(['api.key']) を付ける
    • curl -H "X-Api-Key: your-test-key" <http://localhost:8000/api/>... で 200 / 401 を確認する
  2. ログミドルウェアを段階的に広げる
    • まず 1 ルートに log.incoming を付け、storage/logs/laravel.log で記録を確認する
    • 問題なければ appendToGroup('api', ...) へ移す
  3. 認証ルートと突き合わせる
    • Sanctum のトークン付きリクエストで auth:sanctum ルートの 200 / 401 を確認する(Protecting Routes

Source