Skip to content

ルートガード

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 の onBeforeRouteLeaveonBeforeRouteUpdate を使用します。
  • Options API の beforeRouteEnterbeforeRouteLeave は使用しません。
  • 未保存データがある場合にページ離脱を警告するパターンに 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 に保持します。
  • トークン更新が失敗した場合、保存された認証情報をすべて初期化します。

TIENIPIA QUALIFIED STANDARD