タイトル: The page has expired due to inactivity. Please refresh and try again.
SEOタイトル: Laravel「The page has expired due to inactivity」の原因と対処(CSRF / セッション切れ)
この本文には実行可能なタグ等が含まれるため、安全のためHTMLソースとして表示しています。本文データは変更していません。
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;background:#fffbea;border:1px solid #f4c430;margin:1em 0;width:100%"><tr><td style="background:#f4c430;color:#000;font-weight:bold;padding:8px">この記事の要点</td></tr><tr><td style="padding:10px"><ul><li><strong>Laravel の CSRF トークン切れ</strong>が原因。フォーム表示から送信までの間にセッションが失効</li><li>対処1: フォームに <strong><code>@csrf</code></strong> を入れる(POST フォーム必須)</li><li>対処2: <code>config/session.php</code> の <code>lifetime</code> を延長(デフォルト 120 分)</li><li>対処3: 長時間フォームには <strong>Ajax で CSRF トークン再取得</strong>を実装</li><li>対処4: SESSION_DRIVER がファイルだと storage 権限不足で セッションが書けず即失効する → Redis / DB に</li></ul></td></tr></table>
<h2>エラー画面</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-text">419 | Page Expired
The page has expired due to inactivity.
Please refresh and try again.</code></pre>
<p>Laravel の<strong>CSRF (Cross-Site Request Forgery) 保護機構</strong>が、リクエストの <code>_token</code> とサーバ側セッションの <code>_token</code> が一致しないと判断したときに表示します。HTTP ステータスは <strong>419 (Authentication Timeout)</strong>。</p>
<h2>原因と対処の早見表</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e3f2fd"><th>原因</th><th>確認</th><th>対処</th></tr></thead>
<tbody>
<tr><td>フォームに <code>@csrf</code> 無し</td><td>form タグ内を確認</td><td><code>@csrf</code> 追加</td></tr>
<tr><td>セッション失効</td><td>表示後の経過時間</td><td><code>SESSION_LIFETIME</code> 延長</td></tr>
<tr><td>storage/framework/sessions 権限不足</td><td><code>ls -la storage/framework/sessions</code></td><td>権限修正 or Redis 化</td></tr>
<tr><td>SESSION_DOMAIN ミス</td><td><code>.env</code> の値</td><td>ドメイン合わせる</td></tr>
<tr><td>HTTPS 化したのに <code>SESSION_SECURE_COOKIE</code> 未設定</td><td>.env / Chrome DevTools のCookies</td><td><code>SESSION_SECURE_COOKIE=true</code></td></tr>
<tr><td>マルチドメインで Cookie 共有失敗</td><td>サブドメイン構成</td><td><code>SESSION_DOMAIN=.example.com</code></td></tr>
</tbody>
</table>
<h2>対処1: フォームに <code>@csrf</code> を追加</h2>
<p>POST / PUT / PATCH / DELETE フォームには<strong>必ず</strong> <code>@csrf</code> を入れます:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html">{{-- ❌ NG: トークン無し → 419 --}}
<form method="POST" action="/login">
<input name="email">
<input name="password" type="password">
<button>Login</button>
</form>
{{-- ✅ OK --}}
<form method="POST" action="/login">
@csrf
<input name="email">
<input name="password" type="password">
<button>Login</button>
</form>
{{-- @csrf の展開後 --}}
<input type="hidden" name="_token" value="abc123..."></code></pre>
<h3>Ajax / fetch の場合</h3>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html">{{-- HTML <head> に --}}
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': token, // ★ これ
'Accept': 'application/json',
},
body: JSON.stringify({ name: 'test' }),
});
// axios の場合は自動設定(resources/js/bootstrap.js)
// window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token;
</script></code></pre>
<h2>対処2: セッション寿命を延長</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-php">// config/session.php
'lifetime' => env('SESSION_LIFETIME', 120), // 分単位、デフォルト 120 分
'expire_on_close' => false, // ブラウザ閉じても保持</code></pre><pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-ini"># .env
SESSION_LIFETIME=480 # 8 時間</code></pre>
<p>長時間入力フォーム(記事作成画面など)には<strong>定期的に CSRF トークンを再取得</strong>するのが本筋:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">// 10 分おきにトークンをリフレッシュ
setInterval(async () => {
const res = await fetch('/csrf-token'); // 専用エンドポイント
const data = await res.json();
document.querySelector('meta[name="csrf-token"]').content = data.token;
document.querySelector('input[name="_token"]').value = data.token;
}, 600 * 1000);</code></pre><pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-php">// routes/web.php
Route::get('/csrf-token', fn() => response()->json(['token' => csrf_token()]));</code></pre>
<h2>対処3: storage 権限の確認</h2>
<p>セッションドライバが <code>file</code> で storage が書けないと、毎リクエスト新セッションになり即 419:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-bash"># 確認
ls -la storage/framework/sessions
# drwxr-xr-x www-data:www-data の存在を確認
# 修正
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
# Laravel Sail
./vendor/bin/sail artisan storage:link</code></pre>
<h3>セッションドライバを Redis / DB へ</h3>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-ini"># .env
SESSION_DRIVER=redis # おすすめ(高速 + クラスタ対応)
SESSION_DRIVER=database # 代替
SESSION_DRIVER=cookie # 暗号化 Cookie に格納(容量制限あり)</code></pre><pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-bash"># DB ドライバ使用時
php artisan session:table
php artisan migrate</code></pre>
<h2>対処4: HTTPS / マルチドメインでの Cookie 設定</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-ini"># .env
# HTTPS のみ Cookie 送信(本番では必須)
SESSION_SECURE_COOKIE=true
# サブドメイン間で Cookie 共有
SESSION_DOMAIN=.example.com
# SameSite 制御
SESSION_SAME_SITE=lax # lax / strict / none
# none を使う場合は SECURE_COOKIE=true 必須</code></pre>
<h2>対処5: 特定の URL を CSRF 検証から除外</h2>
<p>外部 Webhook 受信 / 決済リダイレクト等、CSRF トークンを付けられない POST がある場合:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-php">// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
protected $except = [
'/webhook/stripe',
'/webhook/*',
'/payment/notify',
];
}</code></pre>
<p><strong>注意</strong>: 除外は最小限に。外部 API の署名検証などで別途守る必要があります。</p>
<h2>トラブルシュート手順</h2>
<ol>
<li>Chrome DevTools → Application → Cookies で <code>XSRF-TOKEN</code> と <code>laravel_session</code> が存在するか</li>
<li>送信時の Form Data に <code>_token</code> が含まれているか確認</li>
<li><code>storage/logs/laravel.log</code> に詳細エラーが出ていないか</li>
<li><code>storage/framework/sessions/</code> 直下にファイルが書かれるか(file ドライバの場合)</li>
<li>クリックして送信するまでの<strong>滞在時間が SESSION_LIFETIME を超えていないか</strong></li>
</ol>
<h2>FAQ</h2>
<p><strong>Q: ブラウザを開きっぱなしで翌日送信したら 419</strong><br />
A: 期待動作です。長時間の場合は Ajax で定期的にトークン更新するか、<code>SESSION_LIFETIME</code> を延長してください。</p>
<p><strong>Q: ログイン後すぐ 419 になる</strong><br />
A: <code>SESSION_DRIVER=file</code> + storage 権限不足のパターンが多いです。Redis / DB ドライバを推奨。</p>
<p><strong>Q: SPA で 419 が出る</strong><br />
A: Laravel Sanctum を使う場合は <code>config/sanctum.php</code> の <code>stateful</code> ドメイン設定と、フロントの <code>withCredentials: true</code> を確認。</p>