MeWrite Docs

TokenMismatchException

CSRFトークンが一致しない場合に発生するエラー

概要

TokenMismatchException は、LaravelのCSRF(Cross-Site Request Forgery)保護機能により、リクエストに含まれるCSRFトークンがセッションのトークンと一致しない場合に発生するエラーです。

エラーメッセージ

Illuminate\Session\TokenMismatchException
CSRF token mismatch.
419 | Page Expired

原因

  1. CSRFトークンの欠落: フォームに @csrf が含まれていない
  2. セッションの期限切れ: ユーザーのセッションが切れた
  3. ブラウザのキャッシュ: 古いページがキャッシュされている
  4. 複数タブ・ウィンドウ: 別タブでログアウト後に送信
  5. Ajaxリクエストの設定ミス: CSRFトークンがヘッダーにない

解決策

1. フォームにCSRFトークンを追加

{{-- Blade テンプレート --}}
<form method="POST" action="/submit">
    @csrf  {{-- CSRFトークンを追加 --}}
    <input type="text" name="name">
    <button type="submit">送信</button>
</form>

{{-- または手動で追加 --}}
<form method="POST" action="/submit">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
    <input type="text" name="name">
    <button type="submit">送信</button>
</form>

2. Ajax リクエストでの設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// メタタグを追加(layout.blade.php)
<meta name="csrf-token" content="{{ csrf_token() }}">

// axios のデフォルト設定
axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content;

// 個別のリクエスト
axios.post('/api/submit', data, {
    headers: {
        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
    }
});

// jQuery での設定
$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

// fetch API
fetch('/api/submit', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
    },
    body: JSON.stringify(data)
});

3. 特定のルートをCSRF検証から除外

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// app/Http/Middleware/VerifyCsrfToken.php
namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * CSRF検証から除外するURI
     */
    protected $except = [
        'webhook/*',      // Webhook用
        'api/external/*', // 外部API用
        'stripe/*',       // Stripe Webhook用
    ];
}

4. セッション設定の確認

1
2
3
4
5
# .env
SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_DOMAIN=.example.com  # サブドメイン共有時
SESSION_SECURE_COOKIE=true   # HTTPS環境
1
2
3
4
5
6
7
8
9
// config/session.php
'driver' => env('SESSION_DRIVER', 'file'),
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => false,
'encrypt' => false,
'domain' => env('SESSION_DOMAIN'),
'secure' => env('SESSION_SECURE_COOKIE'),
'http_only' => true,
'same_site' => 'lax',

5. セッションストレージの確認

1
2
3
4
5
6
7
8
9
# ファイルセッションの権限確認
ls -la storage/framework/sessions/

# 権限を修正
chmod -R 775 storage/framework/sessions/
chown -R www-data:www-data storage/framework/sessions/

# Redis セッションの場合
redis-cli PING  # 接続確認

6. ロードバランサー環境での対応

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// app/Http/Middleware/TrustProxies.php
namespace App\Http\Middleware;

use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;

class TrustProxies extends Middleware
{
    protected $proxies = '*';  // すべてのプロキシを信頼

    protected $headers =
        Request::HEADER_X_FORWARDED_FOR |
        Request::HEADER_X_FORWARDED_HOST |
        Request::HEADER_X_FORWARDED_PORT |
        Request::HEADER_X_FORWARDED_PROTO |
        Request::HEADER_X_FORWARDED_AWS_ELB;
}

7. SPA(Single Page Application)での対応

1
2
3
4
5
6
// Laravel Sanctum を使用
// 初回にCSRFクッキーを取得
await axios.get('/sanctum/csrf-cookie');

// その後のリクエストは自動的にトークンが付与される
await axios.post('/api/user', userData);
1
2
3
4
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/user', [UserController::class, 'store']);
});

8. エラーハンドリング

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/Exceptions/Handler.php
use Illuminate\Session\TokenMismatchException;

public function render($request, Throwable $exception)
{
    if ($exception instanceof TokenMismatchException) {
        // Ajaxリクエストの場合
        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'セッションが切れました。ページを再読み込みしてください。'
            ], 419);
        }

        // 通常のリクエストの場合
        return redirect()
            ->back()
            ->withInput()
            ->with('error', 'セッションが切れました。再度お試しください。');
    }

    return parent::render($request, $exception);
}

9. JavaScript でのエラーハンドリング

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
axios.interceptors.response.use(
    response => response,
    error => {
        if (error.response?.status === 419) {
            // CSRFトークンエラー
            alert('セッションが切れました。ページを再読み込みします。');
            window.location.reload();
        }
        return Promise.reject(error);
    }
);

デバッグのコツ

トークンの確認

{{-- セッションのトークンを確認 --}}
{{ csrf_token() }}
{{ session()->token() }}
1
2
3
4
5
6
// コントローラーで確認
dd([
    'session_token' => session()->token(),
    'request_token' => request()->input('_token'),
    'header_token' => request()->header('X-CSRF-TOKEN'),
]);

セッションの確認

1
2
3
4
5
// セッションIDを確認
dd(session()->getId());

// セッションデータを確認
dd(session()->all());

Laravel の他のエラー

最終更新: 2025-12-08