Laravel 419エラー解決早見表
Laravelの419エラーの解決早見表です。確認ポイントを紹介します。
関連動画
質問やフィードバック
この記事や動画に関する質問やフィードバックあれば、動画のコメント欄にてお気軽にコメントしてください。
Laravel 419 エラー解決早見表
419 PAGE EXPIREDが出たら、まず送信方法から確認してください。
詳細については、後述される見出しを読んでみてください!
| 症状・状況 | チェックすべきこと | 対処法・確認の進め方 |
|---|---|---|
| Blade のフォームで POST するとき | 送信する form に@csrfがあるか、_tokenが送信されているか | form 内に@csrfを追加し、ページを開き直して送信 |
| Laravel 内の Ajax・fetch・jQuery・Axios で POST するとき | meta タグとX-CSRF-TOKENヘッダー | meta タグから取得したトークンをヘッダーに付与 |
| Sanctum + Axios の SPA のとき | CSRF 初期化、XSRF-TOKEN Cookie、X-XSRF-TOKENヘッダー | /sanctum/csrf-cookieの取得後にログインなどを実行 |
| フロントエンドと API がサブドメインで分かれているとき | CORS と Axios の Cookie 送信設定 | supports_credentials、withCredentials、withXSRFTokenを確認 |
| API が完全に別ドメインにある | サブドメイン構成と混同していないか | API トークンでやる |
| CSRF とセッションの関係を切り分けたい | VerifyCsrfTokenとStartSession | 下記の実験結果を参照し、保護を外すことを解決策にしない |
@csrfを書いたのにフォーム送信で 419 になる | form の外に置いていないか、検索フォームにもあるか | 送信対象の form 内へ配置する方法が紹介されている |
| 長時間放置した画面や複数タブで 419 になる | セッションの期限、別タブでのログイン・ログアウト | ページ再取得や再ログイン、SESSION_LIFETIMEの確認が候補 |
| 設定変更後も 419 が続く | 設定キャッシュなどが残っていないか | config:clearやcache:clearが候補 |
| Cookie が保存 されず、設定ファイルを追加・編集している | PHP 設定ファイルの先頭に空行がないか | <?phpを 1 行目に直した事例あり |
| トークンがあってもセッションが継続しない | StartSessionの適用、Cookie の属性、Sanctum の設定 | ルートのミドルウェアと Cookie の保存・送信を確認する方法が候補 |
| ロードバランサー・複数サーバー構成で 419 になる | サーバー間でセッションを共有しているか | Redis など共通の保存先を使う方法が候補 |
| 419 発生時の案内を変えたい | 例外処理の場所と Laravel のバージョン | TokenMismatchExceptionの捕捉などが紹介されている |
| 外部サービスからの Webhook で CSRF 検証に失敗する | ブラウザのフォームと同じ保護方式が適切か | 特定ルートの除外と送信元の署名検証を検討 |
解決方法
1. Blade のフォームでは form 内に@csrf を追加して解決しました
フォームで、@csrfを付けずに POST すると、次のエラーになりました。
419 PAGE EXPIRED送信する form の中に@csrfを追加して解決しました。
<form method="POST" action="{{ route('post') }}">
@csrf
<input name="post" />
<button type="submit">post</button>
</form>@csrfからは、次のような hidden の input タグが生成されます。
<input
type="hidden"
name="_token"
value="vond93ovKGBBXpALpxAu4Ka9V646MW8tm9BvLRFp"
/>確認するときは、ブラウザの検証を使って、
- HTML に
name="_token"があること - 開発者ツールの Network で送信データに
_tokenが含まれていること
を見ると良いです。
2. Laravel 内の Ajax では meta タグのトークンをヘッダーに付けて解決しました
form を使わない非同期通信では、meta タグに埋め込んだトークンをX-CSRF-TOKENヘッダーに付与する方法で対応しました。
まず、Blade で描画するページの<head>内に、次の meta タグを追加しました。
(以降の JavaScript,jQuery,Vue すべてに以下のコードが必要です)
<head>
<!-- ほかのタグ -->
<meta name="csrf-token" content="{{ csrf_token() }}" />
</head>JavaScript(fetch) の場合
document.querySelectorで meta タグからトークンを取得し、fetch のヘッダーに付与します。
function post() {
const url = "/post"
fetch(url, {
method: "POST",
headers: {
"X-CSRF-TOKEN": document
.querySelector('meta[name="csrf-token"]')
.getAttribute("content"),
"Content-Type": "application/json",
},
body: JSON.stringify({
// 送信するデータ
}),
})
.then(res => {})
.catch(err => console.log(err))
}jQuery を読み込んでいないページでも、このコードだけで動きます。
jQuery(ajax) の場合
jQuery の$.ajaxでも、同じ meta タグから取得したトークンを付与すれば OK です。
let url = "/post"
$.ajax({
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
},
url: url,
type: "POST",
data: {
// 送信するデータ
},
})
.done(function () {})
.fail(function (data) {})Vue などで使う Axios の場合
Axios では毎回ヘッダーを書く手間を省くため、resources/js/bootstrap.jsでまとめて設定しました。
window.axios = require("axios")
window.axios.defaults.headers.common = {
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-TOKEN": document
.querySelector('meta[name="csrf-token"]')
.getAttribute("content"),
}いずれの方法でも、確認するポイントは 送信リクエストのX-CSRF-TOKENが meta タグの値と一致しているかです。
3. Sanctum + Axios では Cookie 方式で解決しました
Laravel プロジェクト外の JavaScript フレームワークから Sanctum + Axios で通信する構成では、
axios.get("/sanctum/csrf-cookie").then(response => {
// ログイン処理…
})上記のコードを、アプリが初期化される最上流のファイル(例: resources/js/app.js や main.js、Vue なら createApp().mount()する前)に書いてください。
別プロジェクトをサブドメインで運用する場合
config/cors.phpで Cookie を伴う通信を許可すれば OK です。
'supports_credentials' => true,resources/js/bootstrap.jsでは、Axios に次の設定を追加します。
axios.defaults.withCredentials = true
axios.defaults.withXSRFToken = trueLaravel 11.x Sanctum の日本語ドキュメントです。
4. 完全に別ドメインの場合は、API トークンで
サブドメインでの構成では上記の方法を使えましたが、完全に別ドメインを API サーバーとして使ったときは、上記の方法では Cookie をセットできませんでした。
なので、API トークンを使った実装でやります。
トークンをバックエンドから送信する API を作ります。
$token = $user->createToken('frontend')->plainTextToken;
return response()->json([
'token' => $token,
]);API から受け取った token を、localStorage 等に保存しておきます。
axios で送信する時の headers にこんな感じで token を付与してあげれば OK です。
await axios.get("https://backend-api.com/api/user", {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
})トークンの保存場所や保有時間などセキュリティ設定が難しい構成です。
特に理由がなければ同一ドメイン(サブドメイン)運用が楽ですね。
よくあるミス
@csrf が form の外にある・検索フォームにない
確認時は、ページ内のどこかに@csrfがあるかだけでなく、実際に送信する form の中にあるかを見てください。
セッション期限切れ・複数タブでの状態の変化
確認時は、config/session.phpのlifetimeとSESSION_LIFETIME、放置時間、別タブでのログイン・ログアウトの順序を記録してください。
入力内容を控えたうえでページを開き直す、必要なら再ログインして送信する、といった切り分けが候補になるようです。
設定変更が反映されない・キャッシュが残っている
config/cors.php などを編集した結果が反映されてなかったりするパターンがあるので、
php artisan cache:clear
php artisan config:clearを実行してみましょう。
ロードバランサー環境でセッションを共有していない
バックエンドサーバーが複数あり、ロードバランサーにて振り分けている場合はセッションをサーバー間で共有する仕組みにしなければなりません。
自分は、こういう時 Redis でセッション管理すると楽なので使います。
Laravel では、セッションで Redis を使う時以下の.envの項目を触れば OK です。
SESSION_DRIVER=redis
SESSION_CONNECTION=default
SESSION_LIFETIME=120
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DB=0Laravel の公式資料でも、複数の Web サーバーでは Redis やデータベースなどの共通保存先を選ぶことが勧められているようです(公式 Session 資料)。
私自身は試していません。
仕組みの説明
Middleware にて、CSRF 保護とセッションの関係を定義してあります。
当時の構成では、routes/web.phpに適用されるwebミドルウェアグループに CSRF 保護が設定されています。
試しに、その2つの設定を以下のようにコメントアウトしてみます。
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
// \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];フォームからも@csrfを取りました。
<form method="POST" action="{{ route('post') }}">
<input name="post" />
<button type="submit">post</button>
</form>この状態で POST すると、419 エラーが発生しません。
VerifyCsrfTokenを外すことで CSRF 保護が動かなくなったということです。
また、StartSessionの Middleware を外すと、CSRF トークンが生成されなくなりました。
419 エラーを消すために、これらのミドルウェアを外すことは勧めません。
この仕組みについては、Laravel 10 以前の構成で検証した記録となり、Laravel11 以降では、別のところに書いてあるみたいですが、まだ検証してません。




