🫠
Laravel + Inertia.js でログアウト後のブラウザバック防止を完全解決
前回の記事でログアウト後のブラウザバック防止を解決できましたが、また、新たな問題として、ログイン中にブラウザバックをすると無限リロードが発生する問題に遭遇しました(泣)時間をかけて解決することができたので、記事にしておこうと思います。
対象読者
・Laravel + Inertia.js で SPA を構築している人
・「ログアウト後に戻るボタンでダッシュボードへ戻れてしまう」問題に悩んでいる人
・無限リロード問題で困っている人
目次
問題の背景
SPAアプリケーションにおいて、以下のようなセキュリティ上の問題が発生することがあります:
- ユーザーが保護されたページ(例:ダッシュボード)にアクセス
- ログアウトを実行
- ブラウザの戻るボタンをクリック
- キャッシュされた保護ページが表示されてしまう
さらに、この問題を解決しようとすると、無限リロードという新たな問題が発生することがあります。
従来のアプローチとその限界
従来の実装例
// ❌ 問題のある実装例
window.addEventListener('popstate', (e) => {
const auth = (router as any).page?.props?.auth;
if (!auth?.user && location.pathname.startsWith('/dashboard')) {
e.preventDefault(); // ← これは効かない
location.replace(route('login')); // ← 無限ループの原因
}
});
問題点
-
preventDefault()が無効-
popstateイベントではpreventDefault()が効かない - ブラウザの履歴操作は既に完了しているため
-
-
無限ループの発生
-
location.replace()で履歴が変更される - これが新しい
popstateイベントを発生させる - 条件が満たされると再び
location.replace()が実行される
-
-
サーバーログでの確認
/login → /dashboard → /login → /dashboard → /login → /dashboard...
決定版ソリューション
核心は「セッションストレージベースの制御」
従来の popstate イベントに依存するアプローチを捨て、セッションストレージを使用した制御に変更します。
解決のポイント
- ログアウト時にセッションストレージにフラグを設定
- ページアクセス時にフラグをチェック
- フラグが存在する場合はログイン画面にリダイレクト
popstateイベントを完全に無効化
実装手順
1. ログアウト処理の修正
// resources/js/components/user-menu-content.tsx
const handleLogout = () => {
cleanup(); // モバイルナビゲーションの後始末
// ログアウト後のリダイレクトフラグをセッションストレージに設定
// このフラグは、ログアウト後にダッシュボードにアクセスしようとした際に
// ログイン画面にリダイレクトするために使用されます
sessionStorage.setItem('logout_redirect', 'true');
// 履歴を置き換えてログアウトを実行
// replace: true により、現在の履歴エントリを新しいものに置き換え
// これにより、ログアウト後に戻るボタンでダッシュボードに戻れなくなります
router.post(route('logout'), {
replace: true, // 履歴を置き換え(新しいエントリを追加しない)
preserveState: false, // 状態を保持しない
preserveScroll: false, // スクロール位置を保持しない
});
};
2. ブラウザバック防止機能の実装
// resources/js/app.tsx
/**
* ログアウト後のブラウザバック防止機能
*
* セッションストレージを使用して、ログアウト後にブラウザの戻るボタンで
* ダッシュボードに戻ることを防ぎます。
*
* 動作原理:
* 1. ログアウト時にセッションストレージにフラグを設定
* 2. ページアクセス時にフラグをチェック
* 3. フラグが存在する場合はログイン画面にリダイレクト
*/
const preventBackAfterLogout = () => {
// セッションストレージからログアウト後のリダイレクトフラグを取得
const isLogoutRedirect = sessionStorage.getItem('logout_redirect');
// ログアウトフラグが存在し、かつダッシュボードにアクセスしようとしている場合
if (isLogoutRedirect && location.pathname.startsWith('/dashboard')) {
console.log('⚠️ ログアウト後のリダイレクトフラグを検知、ログイン画面にリダイレクト');
sessionStorage.removeItem('logout_redirect'); // フラグを削除(一度だけ実行)
// ログイン画面にリダイレクト(エラーハンドリング付き)
try {
window.location.href = route('login');
} catch (error) {
console.error('ログアウト後リダイレクトエラー:', error);
window.location.href = '/login'; // フォールバック
}
return;
}
// 初期表示時(ページリロード直後)の認証チェック
if (location.pathname.startsWith('/dashboard')) {
const auth = (router as any).page?.props?.auth;
if (!auth?.user) {
console.log('⚠️ 初期表示で未認証を検知、ログイン画面にリダイレクト');
try {
window.location.href = route('login');
} catch (error) {
console.error('初期リダイレクトエラー:', error);
window.location.href = '/login'; // フォールバック
}
}
}
};
3. Inertia.js のページ遷移時の認証チェック
// resources/js/app.tsx
/**
* Inertia.js のページ遷移時の認証チェック
*
* ユーザーがページ間を移動する際に、認証状態をチェックし、
* 未認証でダッシュボードにアクセスしようとした場合は
* ログイン画面にリダイレクトします。
*/
router.on('navigate', (event) => {
// Inertia イベントからページ情報を取得
const page: any = event.detail.page;
// セッションストレージからログアウト後のリダイレクトフラグをチェック
const isLogoutRedirect = sessionStorage.getItem('logout_redirect');
// ログアウトフラグが存在し、ダッシュボードへの遷移を試みている場合
if (isLogoutRedirect && page.url.startsWith('/dashboard')) {
event.preventDefault(); // 遷移を停止
console.log('⚠️ navigate でログアウト後リダイレクトフラグを検知、ログイン画面にリダイレクト');
sessionStorage.removeItem('logout_redirect'); // フラグを削除
// ログイン画面にリダイレクト(エラーハンドリング付き)
try {
window.location.href = route('login');
} catch (error) {
console.error('navigate リダイレクトエラー:', error);
window.location.href = '/login'; // フォールバック
}
return;
}
// 通常の認証チェック:未認証でダッシュボードにアクセスしようとしている場合
if (!page.props?.auth?.user && page.url.startsWith('/dashboard')) {
event.preventDefault(); // 遷移を停止
console.log('⚠️ navigate で未認証を検知、ログイン画面にリダイレクト');
// ログイン画面にリダイレクト(エラーハンドリング付き)
try {
window.location.href = route('login');
} catch (error) {
console.error('navigate リダイレクトエラー:', error);
window.location.href = '/login'; // フォールバック
}
}
});
動作原理の詳細解説
🎯 セッションストレージの活用
従来の問題
新しい解決策
🔧 実装の流れ
1. ログアウト時
// 1. セッションストレージにフラグを設定
sessionStorage.setItem('logout_redirect', 'true');
// 2. 履歴を置き換えてログアウト
router.post(route('logout'), { replace: true });
2. ページアクセス時
// 1. セッションストレージからフラグを取得
const isLogoutRedirect = sessionStorage.getItem('logout_redirect');
// 2. フラグが存在する場合はリダイレクト
if (isLogoutRedirect && location.pathname.startsWith('/dashboard')) {
sessionStorage.removeItem('logout_redirect'); // フラグを削除
window.location.href = route('login'); // リダイレクト
}
🛡️ セキュリティの二重チェック
-
初期表示時のチェック
- ページリロード直後の認証状態を確認
- 未認証の場合は即座にリダイレクト
-
ページ遷移時のチェック
- Inertia.js の
navigateイベントで遷移を監視 - 未認証でのダッシュボードアクセスを防止
- Inertia.js の
✅ 実装時の注意点
-
エラーハンドリング
try { window.location.href = route('login'); } catch (error) { window.location.href = '/login'; // フォールバック } -
フラグの適切な管理
// フラグを設定 sessionStorage.setItem('logout_redirect', 'true'); // フラグを削除(一度だけ実行) sessionStorage.removeItem('logout_redirect'); -
ログ出力
console.log('⚠️ ログアウト後のリダイレクトフラグを検知');
🔍 デバッグのポイント
-
ブラウザの開発者ツールでセッションストレージを確認
Application → Session Storage → logout_redirect -
コンソールログで動作を確認
⚠️ ログアウト後のリダイレクトフラグを検知 ⚠️ navigate でログアウト後リダイレクトフラグを検知 -
サーバーログで無限リロードを確認
/login → /dashboard → /login → /dashboard...
まとめ
🎉 解決できた問題
| 問題 | 従来のアプローチ | 新しいソリューション |
|---|---|---|
| ログアウト後のブラウザバック | 部分的に解決 | ✅ 完全解決 |
| 無限リロード | ❌ 発生 | ✅ 完全解決 |
| ユーザー体験 | 悪い | ✅ 良好 |
🚀 このソリューションの利点
-
完全なセキュリティ
- ログアウト後のダッシュボード再表示を100%防止
- 一瞬の表示すら許さない
-
安定性
- 無限リロードのリスクなし
- エラーハンドリング付き
-
保守性
- 明確な実装パターン
- デバッグのしやすさ
-
パフォーマンス
- 軽量な実装
- 不要なイベントリスナーなし
📚 学んだこと
- セッションストレージの活用で、ブラウザの履歴に依存しない制御が可能
-
popstateイベントは無限ループの原因となるため、代替手段を検討すべき
本当に時間がかかり、悩まされました、、、実務未経験だとこの方法があっているかどうかも疑問ですが、とりあえず先に進みたいと思います!どなたか、こうやったらシンプルに解決するよという方法を知っておられるお優しい方がいらっしゃいましたら、ご教授いただけますと幸いです。
Discussion