ルートガード
12.2.1. グローバルガード
- グローバルガードは
router.beforeEachを使用して登録します。 - Vue Router 4 では
next()コールバックの代わりに戻り値を使用します。next()は使用しません。 - ナビゲーションを許可するには
trueまたはundefinedを返します。 - ナビゲーションをブロックするには
falseまたはリダイレクト先パスを返します。
typescript
// src/router/index.ts
router.beforeEach((to, from) => {
// 認証が不要なページは通過
if (!to.meta.requiresAuth) {
return true
}
const authStore = useAuthStore()
if (!authStore.isAuthenticated) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
return true
})router.afterEachはページタイトルの設定、アナリティクスイベントの送信などに使用します。
typescript
router.afterEach((to) => {
document.title = `${to.meta.title} | TQS`
})12.2.2. ルートごとのガード
- 特定のルートにのみ適用されるガードは
beforeEnterプロパティを使用します。 - 権限ベースのアクセス制御は
meta.rolesと組み合わせてルートごとのガードで実装します。 - ガードロジックが複雑な場合は別の関数に分離します。
typescript
// src/router/guards/roleGuard.ts
import type { NavigationGuardWithThis } from 'vue-router'
import { useAuthStore } from '@/stores/useAuthStore'
export function requireRoles(
allowedRoles: string[]
): NavigationGuardWithThis<undefined> {
return () => {
const authStore = useAuthStore()
const userRole = authStore.user?.role
if (!userRole || !allowedRoles.includes(userRole)) {
return { name: 'Forbidden' }
}
return true
}
}typescript
// src/router/routes.ts
import { requireRoles } from './guards/roleGuard'
{
path: '/admin/dashboard',
name: 'AdminDashboard',
component: () => import('@/views/admin/DashboardView.vue'),
beforeEnter: requireRoles(['admin', 'super-admin']),
meta: { title: '管理者ダッシュボード', requiresAuth: true, roles: ['admin'] },
}12.2.3. コンポーネント内ガード
- コンポーネント内ガードは Composition API の
onBeforeRouteLeaveとonBeforeRouteUpdateを使用します。 - Options API の
beforeRouteEnter、beforeRouteLeaveは使用しません。 - 未保存データがある場合にページ離脱を警告するパターンに
onBeforeRouteLeaveを使用します。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
const hasUnsavedChanges = ref(false)
onBeforeRouteLeave(() => {
if (hasUnsavedChanges.value) {
const answer = window.confirm('保存されていない変更があります。移動しますか?')
if (!answer) {
return false
}
}
})
onBeforeRouteUpdate(async (to) => {
// 同じコンポーネントでパラメータのみ変更された場合にデータを再読み込み
await fetchData(to.params.id as string)
})
</script>12.2.4. 認証ガードの実装
- 認証ガードは以下の順序で動作しなければなりません。
| ステップ | 動作 | 失敗時 |
|---|---|---|
| 1 | トークンの存在確認 | ログインページへリダイレクト |
| 2 | トークンの有効性検証 | トークン更新を試行 |
| 3 | ユーザー情報の読み込み | ログインページへリダイレクト |
| 4 | 権限の検証 | 403 ページへリダイレクト |
typescript
// src/router/guards/authGuard.ts
import { useAuthStore } from '@/stores/useAuthStore'
export async function authGuard(to: RouteLocationNormalized) {
if (!to.meta.requiresAuth) {
return true
}
const authStore = useAuthStore()
// ステップ1: トークン確認
if (!authStore.token) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
// ステップ2: トークンの有効性検証および更新
if (authStore.isTokenExpired) {
const refreshed = await authStore.refreshToken()
if (!refreshed) {
authStore.clearAuth()
return { name: 'Login', query: { redirect: to.fullPath } }
}
}
// ステップ3: ユーザー情報の読み込み
if (!authStore.user) {
try {
await authStore.fetchUser()
} catch {
authStore.clearAuth()
return { name: 'Login', query: { redirect: to.fullPath } }
}
}
// ステップ4: 権限の検証
const requiredRoles = to.meta.roles
if (requiredRoles && !requiredRoles.includes(authStore.user.role)) {
return { name: 'Forbidden' }
}
return true
}typescript
// src/router/index.ts
import { authGuard } from './guards/authGuard'
router.beforeEach(authGuard)- 認証ガードはグローバル
beforeEachに単一のエントリポイントとして登録します。 - ログインページへのリダイレクト時に元のパスを
query.redirectに保持します。 - トークン更新が失敗した場合、保存された認証情報をすべて初期化します。