Laravel『TokenMismatchException』の原因と対処法
- 作成日 2025.12.18
- その他
TokenMismatchException は、LaravelのCSRF保護(VerifyCsrfTokenミドルウェア)で「送信されたCSRFトークン」と「セッションに保存されているトークン」が一致しないときに発生する。フォーム送信で @csrf を入れ忘れた、AJAXで X-CSRF-TOKEN ヘッダを付けていない、セッションCookieが送れていない(SameSite/ドメイン/HTTPS/サブドメイン)、ロードバランサ配下でセッションが固定されていない、キャッシュやタブの戻る操作で古いトークンが送られているなどが典型パターン。原因は「トークン不一致」そのものより、セッションが期待通り維持できていないケースが多い。
- 1. エラーの出方と発生条件(典型例)
- 2. CSRFの仕組み(何が一致していないのか)
- 3. 原因1:Bladeフォームに @csrf が入っていない
- 4. 原因2:AJAX/Fetch/AxiosでCSRFトークンヘッダが付いていない
- 5. 原因3:APIルートにwebミドルウェアが当たっている(CSRFが有効になっている)
- 6. 原因4:CSRF除外(VerifyCsrfToken の $except)が必要なエンドポイント
- 7. 原因5:セッションCookieが送れていない(SameSite / Secure / ドメイン)
- 8. 原因6:ロードバランサ配下でセッションが固定されていない(別サーバに飛ぶ)
- 9. 原因7:ブラウザの戻る/キャッシュで古いフォームを送信している
- 10. サンプル:AJAX送信のための共通セットアップ(Blade + Fetch)
- 11. チェックリスト(上から順に確認する)
エラーの出方と発生条件(典型例)
ブラウザ上では 419 Page Expired や TokenMismatchException として出ることが多い。
発生条件の典型:
・POST/PUT/PATCH/DELETE を送るフォームにCSRFトークンが無い
・SPA/Fetch/AxiosでCSRFヘッダやXSRF Cookieが付かない
・セッションが切れた/別サーバに飛んだ/別ドメイン扱いになってCookieが送れない
・HTTP→HTTPS切り替え、www有無、サブドメイン移動でCookieが別物になる
・ブラウザの戻る/キャッシュで古いフォームを送信してトークンが古い
・APIルートにwebミドルウェアを当ててしまい、CSRFが効いているのにトークンを送っていない
CSRFの仕組み(何が一致していないのか)
Laravelのwebルートでは、セッションにCSRFトークンが保存され、フォームやヘッダで送られてきたトークンと照合する。
・セッションCookieが送れない → サーバ側は別セッション扱い → トークンが一致しない
・トークンを送っていない → そもそも照合できず失敗
この2パターンが大半。つまり「フォームにトークンが入っているか」と「同じセッションで送れているか」を確認すると原因が見える。
原因1:Bladeフォームに @csrf が入っていない
フォーム送信で最も多い原因。
<!-- NG -->
<form method="POST" action="/profile/update">
<input type="text" name="name">
<button type="submit">Save</button>
</form>対処:必ず @csrf を入れる。
<!-- OK -->
<form method="POST" action="/profile/update">
@csrf
<input type="text" name="name">
<button type="submit">Save</button>
</form>PUT/PATCH/DELETE なら method spoofing も必要。
<form method="POST" action="/posts/1">
@csrf
@method('PUT')
<input type="text" name="title">
<button type="submit">Update</button>
</form>原因2:AJAX/Fetch/AxiosでCSRFトークンヘッダが付いていない
JavaScriptからPOSTする場合は、X-CSRF-TOKEN を付けるか、XSRF-TOKEN Cookieの仕組みを使う。
metaタグを使う例。
<!-- resources/views/layouts/app.blade.php -->
<meta name="csrf-token" content="{{ csrf_token() }}">Fetchでヘッダを付ける例。
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch('/profile/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({ name: 'Taro' })
});Axiosの例(よくある設定)。
import axios from 'axios';
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.defaults.headers.common['X-CSRF-TOKEN'] =
document.querySelector('meta[name="csrf-token"]').content;原因3:APIルートにwebミドルウェアが当たっている(CSRFが有効になっている)
本来は stateless にしたいAPIに web ミドルウェア(セッション+CSRF)が混ざると、トークンが無いAPIリクエストがTokenMismatchExceptionになる。
確認ポイント:
・routes/api.php なのに RouteServiceProvider や middleware 設定で web が付いていないか
・routes/web.php に API を置いていないか
対処案:
・APIは routes/api.php に置いて、apiミドルウェアを使う
・セッションが必要なAPIだけweb配下に置き、CSRFトークンを必ず送る
どうしても外部WebhookなどでCSRFが不要なら、CSRF除外を検討する(次のセクション)。
原因4:CSRF除外(VerifyCsrfToken の $except)が必要なエンドポイント
外部サービスのWebhookや、CSRFトークンを付けられないシステム連携は、CSRF対象から外す必要がある場合がある。
// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
'webhook/*',
'payment/callback',
];注意点:
・除外すると第三者サイトからの不正POSTを防げなくなる
・除外するのは「外部からの正当なPOSTが来る」など理由が明確なものだけ
・代替として署名検証(HMAC)やIP制限、Basic認証、トークン認証を入れる方が安全
原因5:セッションCookieが送れていない(SameSite / Secure / ドメイン)
トークンは正しく埋め込んでいるのに419になる場合、セッションCookieが送れていないケースが多い。
よくある原因:
・HTTP/HTTPSが混在している(Secure Cookieが送れない)
・wwwあり/なし、サブドメイン移動でCookieドメインが一致していない
・SameSite=Lax/Strict により、別サイト経由のPOSTでCookieが落ちる
・SPAが別ドメインで動いていて、CORS/credentials未設定でCookieが送れない
代表的な確認点(.env)。
APP_URL=https://example.com
SESSION_DOMAIN=.example.com
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=laxクロスドメインSPAの場合は、SameSite=None + Secure が必要になることが多い。
SESSION_SAME_SITE=none
SESSION_SECURE_COOKIE=trueさらに、フロント側で credentials を有効にする必要がある。
// fetchの場合
fetch('https://api.example.com/profile/update', {
method: 'POST',
credentials: 'include',
headers: { 'X-CSRF-TOKEN': token },
body: JSON.stringify(data),
});CORS設定(Laravel側)も合わせて整合させる。
原因6:ロードバランサ配下でセッションが固定されていない(別サーバに飛ぶ)
複数台構成で、リクエストが毎回別サーバに振り分けられ、サーバローカルのfileセッションを使っていると、セッションが維持できずトークン不一致が起きやすい。
典型:
・SESSION_DRIVER=file のまま複数台
・sticky session が無い
対処:
・SESSION_DRIVER=redis / database にして共有ストレージへ
# .env
SESSION_DRIVER=redis・どうしても file のままなら sticky session(セッション固定)を導入
運用の安定性を考えると、セッション共有の方がトラブルが減る。
原因7:ブラウザの戻る/キャッシュで古いフォームを送信している
フォームページを開いたまま長時間放置、別タブでログアウト/ログインし直し、戻るで古いページを再送信、などでCSRFが古くなり419が出ることがある。
対処案:
・419エラー時にフォームへリダイレクトして再表示する(ユーザー体験改善)
・重要フォームは「再表示時にトークンを必ず最新にする」
・長時間操作する画面は定期的にページ更新/トークン更新を設計する
サンプル:AJAX送信のための共通セットアップ(Blade + Fetch)
Bladeレイアウトでmetaタグを置き、JSで毎回ヘッダを付与する。
<!-- layout -->
<meta name="csrf-token" content="{{ csrf_token() }}">
// common.js
export function csrfToken() {
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
}
export async function postJson(url, data) {
const res = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken(),
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify(data),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return await res.json();
}チェックリスト(上から順に確認する)
1) どのHTTPメソッドで落ちているか(POST/PUT/PATCH/DELETE)を確認したか
2) Bladeフォームに @csrf が入っているか(PUT/DELETEなら @method も)を確認したか
3) AJAXなら X-CSRF-TOKEN ヘッダ or XSRFトークンが送られているかをDevToolsで確認したか
4) セッションCookieがリクエストに付いているか(SameSite/Domain/Secure/HTTPS)を確認したか
5) APIルートにwebミドルウェアが当たっていないか(routes/api.php と middleware)を確認したか
6) WebhookなどCSRFを付けられないエンドポイントだけを $except に入れているか(安全対策込み)を確認したか
7) 複数台構成ならセッション共有(redis/database)またはsticky session が有効か確認したか
8) 長時間放置や戻る操作で古いトークンを送っていないか(再表示/再試行で直るか)を確認したか
-
前の記事
Laravel『Call to a Member Function on String』の原因と対処法 2025.12.17
-
次の記事
LaravelのPaginationエラーの原因と対処法 2025.12.19
コメントを書く