LaravelでTokenMismatchExceptionが発生したときの対処方法

LaravelでTokenMismatchExceptionが発生したときの対処方法

LaravelのTokenMismatchExceptionは、CSRFトークンの不一致によって発生する代表的な例外の1つ。特にPOST、PUT、PATCH、DELETEのような状態変更系リクエストで起こりやすく、フォーム送信、Ajax通信、ログイン画面、管理画面、外部Webhook受信時などで頻出する。見た目としては419 Page Expiredや500系のように見えることもあるが、実際には「送信されたトークン」と「セッションに保存されているトークン」が一致していないことが原因である場合が多い。対処を速くするには、フォーム・セッション・Cookie・JavaScript送信ヘッダ・除外ルートの5つに分けて確認すると整理しやすい。

TokenMismatchExceptionとは何か

LaravelはCSRF対策として、ユーザーごとのセッションにトークンを保持し、フォームやAjaxから送られたトークンと照合している。これが一致しない場合に、リクエストは拒否される。
つまりTokenMismatchExceptionは「不正アクセス」だけでなく、「正しいユーザーなのにトークンの渡し方が崩れている」場合にも発生する。

このエラーが発生する代表的な条件

発生しやすい条件は次の通り。
・フォームに @csrf を入れていない
・JavaScriptのPOST送信でCSRFヘッダを付けていない
・セッションが切れているのに古い画面を送信した
・複数タブを長時間開いたまま送信した
・ログイン直後やセッション再生成後に古いトークンを送った
・CookieドメインやSameSite設定がずれている
・Webhookのように外部サービスから送られるリクエストにCSRF保護が掛かっている

まず最初に確認すること

最初に見るべきポイントは次の3つ。
・対象リクエストは web ミドルウェア配下か
・フォームまたはJavaScriptでトークンを送っているか
・セッション/Cookie が正しく維持されているか
LaravelのCSRF検証は通常 web グループ側で動くため、APIルートとWebルートが混ざっていると切り分けを誤りやすい。

フォーム送信での原因:@csrf が入っていない

最も多い原因は、BladeフォームにCSRFトークンを埋め込んでいないこと。
Laravelではフォームの中に @csrf を書くことで、hiddenの _token フィールドが自動生成される。

<form method="POST" action="/profile">
    @csrf

    <input type="text" name="name">
    <button type="submit">保存</button>
</form>

これが無いと、POST送信時に高確率でTokenMismatchExceptionになる。
手書きHTMLや古いテンプレートを流用したときに起きやすい。

Ajax・fetch・Axiosでの原因:ヘッダにトークンが無い

JavaScriptから送信する場合、フォームの hidden _token は使われないため、ヘッダでトークンを送る必要がある。
典型的には X-CSRF-TOKEN または X-XSRF-TOKEN を使う。

<meta name="csrf-token" content="{{ csrf_token() }}">

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

発生条件として多いのは、
・GETでは動くのにPOSTだけ落ちる
・Axios設定を消した
・フロントだけ別テンプレートでmetaタグを出していない
といったケース。

Axios利用時の基本設定

Axiosを使う場合は、全リクエストに自動でCSRFヘッダを付ける構成にすると事故が減る。

import axios from 'axios';

axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

const token = document.querySelector('meta[name="csrf-token"]');

if (token) {
    axios.defaults.headers.common['X-CSRF-TOKEN'] = token.getAttribute('content');
}

これを共通の bootstrap.js などに置いておけば、画面ごとに個別設定する必要が減る。

セッション切れが原因になるケース

フォームに @csrf を入れていても、セッション自体が切れていればトークン照合は失敗する。
よくある状況は次の通り。
・長時間開いたフォームをあとで送信
・ログイン後に別端末や別ブラウザでセッションが変わった
・セッションドライバの設定不備で毎回新しいセッションになる
・Redisやfileセッションの保存が壊れている
この場合、フォーム側ではなくセッション維持の問題を疑うべき。

セッション設定の確認ポイント

セッション由来のTokenMismatchExceptionでは、.envconfig/session.php の設定も影響する。
よく見るべき項目は以下。
SESSION_DRIVER
SESSION_DOMAIN
SESSION_SECURE_COOKIE
same_site
・ロードバランサ配下ならHTTPS判定
特に、HTTP/HTTPSの混在やサブドメイン運用ではCookieが想定通り送られず、CSRFトークン不一致になりやすい。

# .env の例
SESSION_DRIVER=file
SESSION_DOMAIN=.example.com
SESSION_SECURE_COOKIE=true

発生条件としては、
・ローカルでは動くのに本番だけ落ちる
・wwwあり/なしで挙動が違う
・管理画面だけ別サブドメイン
などが典型。

ログイン画面や認証周りで起きるケース

ログインやログアウト直後は、Laravelがセッションを再生成するため、古いフォームや古いトークンが残っていると不一致になりやすい。
たとえば次のようなケース。
・ログインページを開いたまま長時間放置して送信
・別タブで再ログイン後、古いタブからPOST
・CSRFトークンをJavaScript変数に保持していて更新していない
この場合は、ページリロードで直ることが多いが、根本的には「古いトークンを送らせない」設計が必要。

Webhookや外部連携で起きるケース

StripeやSlackなどの外部サービスからのWebhookには、LaravelのCSRFトークンは送られない。
それなのに web ミドルウェア配下に置いていると、必ずTokenMismatchExceptionになる。
この種のルートは、CSRF検証から外す必要がある。

Laravelのバージョンによって設定場所は異なるが、考え方としては「外部から来る正当なコールバックはCSRF保護対象から除外する」。
ただし、何でも除外すると危険なので、Webhook等の必要最小限に限定する。

// 例:Webhookは専用ルートに分ける考え方
Route::post('/webhooks/stripe', [StripeWebhookController::class, 'handle']);

発生条件:
・決済完了後だけ419/TokenMismatchException
・外部サービスのコールバックが届いているのにアプリ側で拒否
・テストツール(Postman等)からPOSTすると落ちる

SPAやフロント分離構成での原因

SPA構成では、CSRFトークンとセッションCookieの両方が揃わないと失敗する。
起きやすい条件は次の通り。
・フロントとAPIでドメインが違う
・Cookieが送信されていない
・CSRF Cookieの取得をしていない
・withCredentials が無い
・CORS設定だけ整えてCSRF側を忘れている
SPAでは「トークンヘッダだけ送ればいい」ではなく、Cookieベース認証ならセッションも成立している必要がある。

設定変更後に直らない場合

Laravelでは設定キャッシュの影響で、.env 修正後も古い設定を読んでいることがある。
セッションやCookie設定を変えたのに改善しない場合は、キャッシュクリアを確認する。

php artisan config:clear
php artisan cache:clear
php artisan route:clear
php artisan view:clear

必要ならまとめて消す。

php artisan optimize:clear

発生条件:
.env を修正したのに本番挙動が変わらない
・セッションドライバ変更後もエラー継続
・Docker再起動後だけ直る

対処の優先順位

TokenMismatchExceptionが出たら、次の順で切り分けると速い。

  1. フォームに @csrf があるか
  2. Ajaxなら X-CSRF-TOKEN を送っているか
  3. セッションが維持されているか
  4. Cookieドメイン・HTTPS・SameSite が合っているか
  5. 外部Webhookを誤ってCSRF対象にしていないか
  6. 設定キャッシュが古くないか
    この順で見ると、だいたいの原因にたどり着ける。

よくある誤った対処

やってはいけない対応も多い。
・CSRF保護を全部無効化する
・原因不明のまま全POSTルートを除外する
・本番でAPP_DEBUG=trueにして調査する
・セッション設定を場当たり的に変え続ける
TokenMismatchExceptionは「セキュリティ機構が働いている結果」でもあるため、雑に無効化するのではなく、正しく通す方向で直すべき。

まとめ

LaravelのTokenMismatchExceptionは、ほとんどの場合「CSRFトークンの送信漏れ」か「セッション/Cookie不整合」で起きる。
対処の基本は、
・フォームには @csrf を入れる
・Ajaxでは X-CSRF-TOKEN を送る
・セッション設定を確認する
・Webhookは必要な範囲だけ除外する
・設定変更後はキャッシュを見直す
この流れを押さえること。
単に例外を消すのではなく、「なぜトークンが一致しなかったのか」を切り分けることが、再発防止にもつながる。