ミドルウェア
Bobは, Aliceが権限チェックなしで/admin管理画面に直接アクセスできることを発見しました。Charlieにはルートガードが必要です。認証されていないユーザーが管理画面にアクセスしようとしたらログインページにリダイレクトし, 一般ユーザーが管理ルートにアクセスするのを防ぐものです。Nuxt 3のミドルウェアシステムがその解決策です。
1. 学ぶ内容
- グローバルミドルウェア:middleware/auth.global.ts - グローバルスコープと実行順序
- 名前付きミドルウェア:
definePageMetaとミドルウェア配列の組み合わせ - インラインミドルウェア:ページ内で直接定義する軽量ミドルウェア
- ミドルウェアとSSR:二重実行メカニズムとクライアント専用ミドルウェア
- MegaShop管理者ルートガード + ロールチェック:実践ガイド
2. 管理者のある本当の話
(1) 課題:権限制御不足による重大な脆弱性
Aliceはブラウザに/adminと入力してMegaShop管理画面にアクセスし, すべての注文とユーザーデータを閲覧できました。Bobは管理者として, 自分が見る画面が一般ユーザーと全く同じであることに気づきました。Charlieはルートレベルのアクセス制御を急務として必要としていました。
(2) ルーティングミドルウェアの解決策
Nuxt 3では, ミドルウェアはルート遷移前に実行され, リダイレクトやアクセスブロックが可能です:
TYPESCRIPT
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
if (!isAuthenticated()) return navigateTo('/login')
})
(3) メリット:ルートレベルのアクセス制御
Aliceは/adminに直接アクセスできなくなり, ログインしていない場合はログインページに自動的にリダイレクトされます。Bobの管理者権限はルーティングレベルで強制されます。
3. 3つのミドルウェアパターン
(1) ミドルウェアの実行順序
flowchart LR
A[ルートナビゲーション] --> B[グローバルミドルウェア 1]
B --> C[グローバルミドルウェア 2]
C --> D[名前付き/インラインミドルウェア]
D --> E[ページレンダリング]
D -->|中断| F[リダイレクト / 中断]
B -->|中断| F
C -->|中断| F
(2) 3つのモデルの比較
| 項目 | グローバルミドルウェア | 名前付きミドルウェア | インラインミドルウェア |
|---|---|---|---|
| ファイル名 | *.global.ts | *.ts ("global"なし) | ファイルなし |
| スコープ | 全ルート | 特定ページ | 現在のページ |
| 定義方法 | middleware/ ディレクトリ | middleware/ ディレクトリ | definePageMeta内 |
| 再利用性 | ✅ 自動再利用 | ✅ 複数ページ参照 | ❌ 現在のページのみ |
| ユースケース | グローバル認証/ログ | ページ固有ガード | 単純な一回限りチェック |
4. グローバルミドルウェア
(1) ▶ サンプル:グローバル認証ミドルウェア
TYPESCRIPT
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// 公開ページは認証をスキップ
const publicPages = ['/', '/products', '/about', '/login', '/register']
if (publicPages.includes(to.path)) return
const token = useCookie('auth-token')
if (!token.value) {
return navigateTo(`/login?redirect=${to.path}`, { redirectCode: 302 })
}
})
出力:
TEXT
// 実行成功
(2) ▶ サンプル:グローバルログミドルウェア
TYPESCRIPT
// middleware/log.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log(`Navigation: ${from.path} → ${to.path}`)
// returnなし = ナビゲーションを許可
})
出力:
TEXT
// 実行成功
⚠️ 注意: グローバルミドルウェアはファイル名のアルファベット順で実行されます。
01-auth.global.tsは02-log.global.tsより先に実行されます。数字プレフィックスを使用して順序を制御することをお勧めします。
5. 名前付きミドルウェア
(1) ▶ サンプル:管理者ロールミドルウェア
TYPESCRIPT
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to) => {
const user = useState('user')
if (!user.value) {
return navigateTo(`/login?redirect=${to.path}`)
}
if (user.value.role !== 'admin') {
// 一般ユーザーが管理画面にアクセスしようとした
throw createError({
statusCode: 403,
message: 'Access denied. Admin role required.'
})
}
})
出力:
TEXT
// 実行成功
(2) ▶ サンプル:ページで名前付きミドルウェアを参照
VUE
<!-- pages/admin/index.vue -->
<template>
<div>
<h1>Admin Dashboard</h1>
<p>Welcome, Bob (Admin)</p>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'admin', // 名前付きミドルウェアを参照
layout: 'sidebar'
})
</script>
出力:
TEXT
// 実行成功
(3) ▶ サンプル:複数ミドルウェアの組み合わせ
VUE
<!-- pages/admin/settings.vue -->
<template>
<div>
<h1>Admin Settings</h1>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'admin'], // 順番に実行:auth → admin
layout: 'sidebar'
})
</script>
出力:
TEXT
// 実行成功
6. インラインミドルウェア
(1) ▶ サンプル:ページ内でミドルウェアを定義
VUE
<!-- pages/checkout.vue -->
<template>
<div>
<h1>Checkout</h1>
<p>Cart total: ${{ total }} USD</p>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: defineNuxtRouteMiddleware((to) => {
// インライン:カートに商品があるかチェック
const cart = useState<any[]>('cart')
if (cart.value.length === 0) {
return navigateTo('/cart')
}
})
})
const cart = useState<any[]>('cart')
const total = computed(() => cart.value.reduce((s, i) => s + i.price, 0))
</script>
出力:
TEXT
// 実行成功
(1) インラインと名前付きミドルウェアの選び方
| シナリオ | 選択 | 理由 |
|---|---|---|
| サイト全体の認証 | グローバル | 全ルートをチェック |
| 管理ルート | 名前付き | 複数の管理ページで共有 |
| カートが空でないかチェック | インライン | 注文ページでのみ使用 |
| VIPエリア | 名前付き | 複数ページで使用する可能性 |
| A/Bテストのトラフィック分割 | インライン | 一回限りのロジック |
7. ミドルウェアとSSR
(1) SSR二重実行メカニズム
flowchart TB
A[ユーザーが/adminにナビゲーション] --> B[サーバー:ミドルウェア実行]
B -->|リダイレクト| C[サーバーがリダイレクトレスポンス送信]
B -->|許可| D[サーバーがページレンダリング]
D --> E[クライアント:ミドルウェア再実行]
E -->|リダイレクト| F[クライアント側リダイレクト]
E -->|許可| G[クライアントがページをハイドレーション]
(1) ▶ サンプル:クライアント専用ミドルウェア
TYPESCRIPT
// middleware/client-only.ts
export default defineNuxtRouteMiddleware((to) => {
// サーバーではスキップ - クライアントのみチェック
if (import.meta.server) return
const localStorage = window.localStorage
const preferences = JSON.parse(localStorage.getItem('user-preferences') || '{}')
if (preferences.theme === 'dark') {
// ブラウザAPIを使用するクライアント専用ロジック
document.documentElement.classList.add('dark')
}
})
出力:
TEXT
// 実行成功
(2) SSRミドルウェアの注意点
| 問題 | 原因 | 解決策 |
|---|---|---|
| ミドルウェアが2回実行される | SSRとクライアントで各1回実行 | 期待される動作;べき等性を確保 |
| window/localStorageの使用 | SSR (ブラウザ環境なし) | if (import.meta.client)でチェック |
| CookieはSSRで利用可能 | リクエストにCookieが含まれる | useCookieで読み取り |
| SSRでのnavigateTo | 302リダイレクトを返す | 期待される動作 |
| ミドルウェア内のuseState | ✅ 利用可能 | ログイン状態チェックに適している |
8. 総合例:MegaShop権限ガードシステム
TYPESCRIPT
// middleware/01-auth.global.ts - グローバル認証チェック
export default defineNuxtRouteMiddleware((to) => {
const publicPaths = ['/', '/products', '/products/**', '/about', '/login', '/register']
const isPublic = publicPaths.some(path => {
if (path.includes('')) return to.path.startsWith(path.replace('/', ''))
return path === to.path
})
if (isPublic) return
const token = useCookie('auth-token')
if (!token.value) {
return navigateTo(`/login?redirect=${encodeURIComponent(to.path)}`)
}
})
TYPESCRIPT
// middleware/admin.ts - 管理者ロールチェック
export default defineNuxtRouteMiddleware((to) => {
const user = useCookie('user-data')
if (!user.value) {
return navigateTo('/login')
}
if ((user.value as any).role !== 'admin') {
throw createError({ statusCode: 403, message: 'Admin access required' })
}
})
VUE
<!-- pages/admin/products/index.vue -->
<template>
<div>
<h1>Product Management</h1>
<p>Manage 1 million products</p>
<!-- 管理者商品CRUDインターフェース -->
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: ['admin'],
layout: 'sidebar'
})
</script>
❓ よくある質問
Q ミドルウェアの実行順序はどう制御しますか?
A グローバルミドルウェアはファイル名のアルファベット順にソートされます。数字プレフィックス (01-xxx.global.ts)の使用をお勧めします。名前付きミドルウェアは
definePageMeta内の配列で指定された順序で実行されます。Q ミドルウェア内で非同期処理は可能ですか?
A
defineNuxtRouteMiddleware自体はasyncをサポートしていません。ただし, ミドルウェア内で同期的なAPI (useCookieやuseStateなど)を呼び出すことは可能です。非同期検証が必要な場合は, プラグインまたはページのsetupメソッドで処理することをお勧めします。Q なぜミドルウェアはSSRで1回, クライアント側で1回実行されるのですか?
A SSRでは, ミドルウェアがルート権限をチェックしてページをレンダリングするかどうかを決定します。クライアント側のハイドレーション時にも再度実行され, 一貫性が確保されます。両方の実行ロジックはべき等である必要があります。
Q グローバルミドルウェアはパフォーマンスに影響しますか?
A すべてのグローバルミドルウェアはルートナビゲーションのたびに実行されます。ミドルウェアのロジックは軽量に保ち, 重い計算やAPI呼び出しをグローバルミドルウェア内で行うのは避けてください。
Q ミドルウェア内で
createErrorがスローされた場合はどうなりますか?A SSRでは, 対応するステータスコードのHTMLエラーページが返されます (例:403ページ)。クライアント側では, Nuxtエラーページが表示されます。
error.vueでカスタマイズ可能です。Q ミドルウェア間でデータを渡すにはどうすればよいですか?
A
useStateまたはNuxtAppのpayloadプロパティを使用します。const nuxtApp = useNuxtApp() → nuxtApp.payload.xxx = data。次のミドルウェアまたはページからアクセスできます。📖 まとめ
- 3種類のミドルウェア:グローバル (全ルート), 名前付き (特定ページ), インライン (現在のページ)
- グローバルミドルウェアはファイル名のアルファベット順で実行されます。数字プレフィックスで順序を制御することをお勧めします
definePageMetaのmiddleware配列参照で複数の名前付きミドルウェアを組み合わせ可能- SSR二重実行は期待される動作です。ブラウザAPIは
import.meta.clientで保護する必要があります - MegaShopはグローバル認証と名前付きadminミドルウェアで完全な権限ガードを実装
📝 練習問題
- 基本問題 (難易度:⭐):
guest.tsという名前のミドルウェアを作成し, 未認証ユーザーのみがページにアクセスできるようにします (ログイン済みユーザーはホームページにリダイレクト)。これを/loginページに適用してください。 - 応用問題 (難易度:⭐⭐):完全な認可システムを実装:グローバル認証チェックでログインを確認 + 「admin」ユーザーのロールチェック。/admin/**ルートを保護します。
- チャレンジ (難易度:⭐⭐⭐):ミドルウェア間のデータ渡しを実装—
authミドルウェアでユーザー情報を取得し,NuxtAppのpayloadに格納。その後adminミドルウェアで直接読み取り, 重複リクエストを回避します。
---|



