T-CREATOR

Nuxt のミドルウェアでアクセス制御&認証を強化する

Nuxt のミドルウェアでアクセス制御&認証を強化する

Web アプリケーションを開発する上で、最も重要な課題の一つがセキュリティです。特に認証とアクセス制御は、ユーザーの信頼を守り、アプリケーションの安全性を確保するために欠かせない要素です。

Nuxt.js のミドルウェア機能を使えば、これらの課題をエレガントに解決できます。この記事では、実際のプロジェクトで使える実践的な認証システムの構築方法をご紹介します。

あなたのアプリケーションが、どのような規模であっても、適切なセキュリティ対策を施すことで、ユーザーに安心して利用していただけるサービスになります。一緒に、堅牢で使いやすい認証システムを作り上げていきましょう。

Nuxt ミドルウェアの基本概念

Nuxt のミドルウェアは、ページやレイアウトがレンダリングされる前に実行される関数です。認証チェック、ログイン状態の確認、権限の検証など、アプリケーション全体で共通して必要な処理を効率的に実装できます。

ミドルウェアの種類

Nuxt には 3 種類のミドルウェアがあります:

  1. グローバルミドルウェア - すべてのページで実行
  2. レイアウトミドルウェア - 特定のレイアウトで実行
  3. ページミドルウェア - 特定のページでのみ実行

基本的なミドルウェアの構造

typescript// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  // 認証チェックのロジック
  const { $auth } = useNuxtApp();

  if (!$auth.isAuthenticated) {
    return navigateTo('/login');
  }
});

このシンプルな構造が、あなたのアプリケーションのセキュリティを支える基盤になります。ミドルウェアの威力は、その実行タイミングにあります。ページが表示される前に確実に認証状態をチェックできるため、未認証ユーザーが保護されたページにアクセスすることを防げます。

認証システムの設計

効果的な認証システムを構築するには、まず設計段階で全体像を把握することが重要です。ここでは、実用的で拡張性のある認証システムの設計について説明します。

認証フローの設計

typescript// types/auth.ts
export interface User {
  id: string;
  email: string;
  role: 'user' | 'admin' | 'moderator';
  permissions: string[];
}

export interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  token: string | null;
}

認証状態の管理

typescript// composables/useAuth.ts
export const useAuth = () => {
  const user = useState<User | null>('user', () => null);
  const token = useState<string | null>(
    'token',
    () => null
  );

  const isAuthenticated = computed(
    () => !!user.value && !!token.value
  );

  const login = async (email: string, password: string) => {
    try {
      const response = await $fetch('/api/auth/login', {
        method: 'POST',
        body: { email, password },
      });

      user.value = response.user;
      token.value = response.token;

      // トークンをローカルストレージに保存
      localStorage.setItem('auth_token', response.token);

      return { success: true };
    } catch (error) {
      console.error('Login error:', error);
      return { success: false, error: error.message };
    }
  };

  return {
    user: readonly(user),
    token: readonly(token),
    isAuthenticated,
    login,
  };
};

この設計により、アプリケーション全体で一貫した認証状態を管理できます。useAuthコンポーザブルを使うことで、どのコンポーネントからでも簡単に認証情報にアクセスできるようになります。

グローバルミドルウェアの実装

グローバルミドルウェアは、アプリケーション全体のセキュリティを守る重要な役割を担います。すべてのページアクセスで認証状態をチェックし、適切なリダイレクト処理を行います。

基本的な認証ミドルウェア

typescript// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
  const { $auth } = useNuxtApp();

  // 認証が不要なページのリスト
  const publicPages = [
    '/login',
    '/register',
    '/forgot-password',
  ];

  // 現在のページが公開ページかチェック
  if (publicPages.includes(to.path)) {
    return;
  }

  // 認証状態をチェック
  if (!$auth.isAuthenticated) {
    // ログインページにリダイレクト
    return navigateTo('/login', {
      query: { redirect: to.fullPath },
    });
  }
});

トークン検証ミドルウェア

typescript// middleware/token-validation.global.ts
export default defineNuxtRouteMiddleware(async (to) => {
  const { $auth } = useNuxtApp();

  // トークンが存在する場合、有効性を検証
  if ($auth.token) {
    try {
      const response = await $fetch('/api/auth/verify', {
        headers: {
          Authorization: `Bearer ${$auth.token}`,
        },
      });

      // トークンが有効な場合、ユーザー情報を更新
      if (response.valid) {
        $auth.user = response.user;
      } else {
        // トークンが無効な場合、ログアウト処理
        $auth.logout();
        return navigateTo('/login');
      }
    } catch (error) {
      console.error('Token validation error:', error);
      // エラーが発生した場合もログアウト
      $auth.logout();
      return navigateTo('/login');
    }
  }
});

グローバルミドルウェアの実装により、アプリケーション全体で一貫したセキュリティポリシーを適用できます。これにより、開発者が個別のページで認証チェックを忘れるリスクを大幅に軽減できます。

ルート別ミドルウェアの活用

特定のページやセクションに応じた細かいアクセス制御が必要な場合、ルート別ミドルウェアが威力を発揮します。ユーザーの権限やロールに基づいて、きめ細かな制御が可能になります。

管理者専用ページのミドルウェア

typescript// middleware/admin.ts
export default defineNuxtRouteMiddleware((to) => {
  const { $auth } = useNuxtApp();

  // 認証チェック
  if (!$auth.isAuthenticated) {
    return navigateTo('/login');
  }

  // 管理者権限チェック
  if ($auth.user?.role !== 'admin') {
    // 権限不足エラーページにリダイレクト
    return navigateTo('/error/403', {
      query: {
        message: '管理者権限が必要です',
        code: 'INSUFFICIENT_PERMISSIONS',
      },
    });
  }
});

権限ベースのミドルウェア

typescript// middleware/permission.ts
export default defineNuxtRouteMiddleware((to) => {
  const { $auth } = useNuxtApp();

  // 必要な権限をメタデータから取得
  const requiredPermissions = to.meta
    .permissions as string[];

  if (!requiredPermissions) {
    return; // 権限要件がない場合は通過
  }

  // ユーザーの権限をチェック
  const userPermissions = $auth.user?.permissions || [];
  const hasPermission = requiredPermissions.every(
    (permission) => userPermissions.includes(permission)
  );

  if (!hasPermission) {
    throw createError({
      statusCode: 403,
      statusMessage: '権限が不足しています',
      fatal: true,
    });
  }
});

ページでの権限設定

vue<!-- pages/admin/dashboard.vue -->
<script setup>
definePageMeta({
  middleware: ['admin'],
  permissions: ['dashboard:read', 'users:manage'],
});
</script>

<template>
  <div>
    <h1>管理者ダッシュボード</h1>
    <!-- 管理者専用コンテンツ -->
  </div>
</template>

ルート別ミドルウェアを使うことで、ページごとに異なるセキュリティ要件を柔軟に設定できます。これにより、複雑な権限構造を持つアプリケーションでも、明確で保守しやすいコードを書けます。

認証状態の管理

認証状態の管理は、ユーザーエクスペリエンスとセキュリティの両方に直結する重要な要素です。適切な状態管理により、ユーザーは快適にアプリケーションを利用でき、開発者は安全なコードを書けます。

プラグインでの認証初期化

typescript// plugins/auth.client.ts
export default defineNuxtPlugin(async () => {
  const { $auth } = useNuxtApp();

  // ページ読み込み時にトークンを復元
  const token = localStorage.getItem('auth_token');

  if (token) {
    try {
      // トークンの有効性を検証
      const response = await $fetch('/api/auth/me', {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });

      $auth.user = response.user;
      $auth.token = token;
    } catch (error) {
      console.error('Token restoration failed:', error);
      // 無効なトークンは削除
      localStorage.removeItem('auth_token');
    }
  }
});

ログアウト処理の実装

typescript// composables/useAuth.ts の続き
const logout = async () => {
  try {
    // サーバーサイドでトークンを無効化
    if (token.value) {
      await $fetch('/api/auth/logout', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${token.value}`,
        },
      });
    }
  } catch (error) {
    console.error('Logout error:', error);
  } finally {
    // ローカル状態をクリア
    user.value = null;
    token.value = null;
    localStorage.removeItem('auth_token');

    // ログインページにリダイレクト
    await navigateTo('/login');
  }
};

認証状態の監視

typescript// composables/useAuthWatcher.ts
export const useAuthWatcher = () => {
  const { $auth } = useNuxtApp();

  // 認証状態の変化を監視
  watch(
    () => $auth.isAuthenticated,
    (newValue, oldValue) => {
      if (!newValue && oldValue) {
        // ログアウト時の処理
        console.log('User logged out');

        // 必要に応じてクリーンアップ処理
        // 例:WebSocket接続の切断、キャッシュのクリアなど
      }
    }
  );

  // ユーザー情報の変化を監視
  watch(
    () => $auth.user,
    (newUser) => {
      if (newUser) {
        // ログイン時の処理
        console.log('User logged in:', newUser.email);

        // 必要に応じて初期化処理
        // 例:通知の設定、データのプリロードなど
      }
    }
  );
};

適切な認証状態管理により、ユーザーはページをリロードしてもログイン状態が維持され、セキュアな操作が継続できます。また、ログアウト時には確実にリソースがクリーンアップされ、セキュリティリスクを最小限に抑えられます。

エラーハンドリングとリダイレクト

エラーハンドリングは、ユーザーエクスペリエンスを向上させ、セキュリティインシデントを適切に処理するために不可欠です。適切なエラー処理により、ユーザーは何が起こったかを理解し、適切な対処法を知ることができます。

認証エラーの処理

typescript// middleware/error-handler.global.ts
export default defineNuxtRouteMiddleware((to) => {
  const { $auth } = useNuxtApp();

  // エラーページでの処理
  if (to.path.startsWith('/error')) {
    return;
  }

  // 認証エラーの処理
  const handleAuthError = (error: any) => {
    console.error('Authentication error:', error);

    if (error.statusCode === 401) {
      // 未認証エラー
      $auth.logout();
      return navigateTo('/login', {
        query: {
          error: 'UNAUTHORIZED',
          message:
            'セッションが期限切れです。再度ログインしてください。',
        },
      });
    }

    if (error.statusCode === 403) {
      // 権限不足エラー
      return navigateTo('/error/403', {
        query: {
          message:
            'このページにアクセスする権限がありません。',
          code: 'FORBIDDEN',
        },
      });
    }
  };

  // グローバルエラーハンドラーを設定
  $auth.handleError = handleAuthError;
});

カスタムエラーページの実装

vue<!-- error.vue -->
<script setup>
const error = useError();
const { $auth } = useNuxtApp();

// エラーコードに応じた処理
const handleError = () => {
  if (error.value?.statusCode === 401) {
    $auth.logout();
  }

  // エラーをクリア
  clearError();

  // ホームページに戻る
  navigateTo('/');
};
</script>

<template>
  <div class="error-container">
    <h1>エラーが発生しました</h1>

    <div
      v-if="error?.statusCode === 401"
      class="error-message"
    >
      <p>セッションが期限切れです。</p>
      <p>再度ログインしてください。</p>
      <button @click="handleError">ログインページへ</button>
    </div>

    <div
      v-else-if="error?.statusCode === 403"
      class="error-message"
    >
      <p>このページにアクセスする権限がありません。</p>
      <p>管理者にお問い合わせください。</p>
      <button @click="handleError">ホームページへ</button>
    </div>

    <div v-else class="error-message">
      <p>予期しないエラーが発生しました。</p>
      <p>しばらく時間をおいて再度お試しください。</p>
      <button @click="handleError">ホームページへ</button>
    </div>
  </div>
</template>

リダイレクト処理の最適化

typescript// composables/useRedirect.ts
export const useRedirect = () => {
  const route = useRoute();

  const redirectAfterLogin = () => {
    const redirectPath = route.query.redirect as string;

    if (redirectPath && redirectPath.startsWith('/')) {
      // 安全なリダイレクト先に移動
      return navigateTo(redirectPath);
    }

    // デフォルトのダッシュボードに移動
    return navigateTo('/dashboard');
  };

  const redirectToLogin = (message?: string) => {
    const currentPath = route.fullPath;

    return navigateTo('/login', {
      query: {
        redirect: currentPath,
        ...(message && { message }),
      },
    });
  };

  return {
    redirectAfterLogin,
    redirectToLogin,
  };
};

適切なエラーハンドリングにより、ユーザーは何が起こったかを理解し、適切な対処法を知ることができます。また、セキュリティインシデントが発生した場合でも、適切に処理され、ユーザーの混乱を最小限に抑えられます。

セキュリティ強化のベストプラクティス

セキュリティは継続的な改善が必要な分野です。ここでは、Nuxt アプリケーションで実践できるセキュリティ強化のベストプラクティスをご紹介します。

CSRF 対策の実装

typescript// middleware/csrf.global.ts
export default defineNuxtRouteMiddleware((to) => {
  // POST、PUT、DELETEリクエストでのCSRF対策
  if (['POST', 'PUT', 'DELETE'].includes(to.method || '')) {
    const csrfToken = useCookie('csrf-token');

    if (!csrfToken.value) {
      throw createError({
        statusCode: 403,
        statusMessage: 'CSRFトークンが無効です',
        fatal: true,
      });
    }
  }
});

レート制限の実装

typescript// middleware/rate-limit.ts
const rateLimitMap = new Map();

export default defineNuxtRouteMiddleware((to) => {
  const { $auth } = useNuxtApp();

  // ログインページでのレート制限
  if (to.path === '/login') {
    const clientId = $auth.user?.id || 'anonymous';
    const key = `login:${clientId}`;

    const attempts = rateLimitMap.get(key) || 0;

    if (attempts >= 5) {
      throw createError({
        statusCode: 429,
        statusMessage:
          'リクエストが多すぎます。しばらく時間をおいてください。',
        fatal: true,
      });
    }

    rateLimitMap.set(key, attempts + 1);

    // 5分後にリセット
    setTimeout(() => {
      rateLimitMap.delete(key);
    }, 5 * 60 * 1000);
  }
});

セキュリティヘッダーの設定

typescript// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    headers: {
      'X-Frame-Options': 'DENY',
      'X-Content-Type-Options': 'nosniff',
      'X-XSS-Protection': '1; mode=block',
      'Referrer-Policy': 'strict-origin-when-cross-origin',
      'Content-Security-Policy':
        "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
    },
  },
});

パスワードポリシーの実装

typescript// utils/password-validation.ts
export const validatePassword = (password: string) => {
  const errors: string[] = [];

  if (password.length < 8) {
    errors.push(
      'パスワードは8文字以上である必要があります'
    );
  }

  if (!/[A-Z]/.test(password)) {
    errors.push('大文字を含める必要があります');
  }

  if (!/[a-z]/.test(password)) {
    errors.push('小文字を含める必要があります');
  }

  if (!/\d/.test(password)) {
    errors.push('数字を含める必要があります');
  }

  if (!/[!@#$%^&*]/.test(password)) {
    errors.push('特殊文字を含める必要があります');
  }

  return {
    isValid: errors.length === 0,
    errors,
  };
};

セッション管理の強化

typescript// composables/useSession.ts
export const useSession = () => {
  const { $auth } = useNuxtApp();

  const refreshSession = async () => {
    if (!$auth.token) return;

    try {
      const response = await $fetch('/api/auth/refresh', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${$auth.token}`,
        },
      });

      $auth.token = response.token;
      localStorage.setItem('auth_token', response.token);
    } catch (error) {
      console.error('Session refresh failed:', error);
      $auth.logout();
    }
  };

  // 定期的なセッション更新
  const startSessionRefresh = () => {
    setInterval(refreshSession, 15 * 60 * 1000); // 15分ごと
  };

  return {
    refreshSession,
    startSessionRefresh,
  };
};

これらのベストプラクティスを実装することで、アプリケーションのセキュリティレベルを大幅に向上させられます。セキュリティは一度実装すれば終わりではなく、継続的な監視と改善が必要です。

まとめ

Nuxt のミドルウェアを使った認証・アクセス制御システムの構築について、実践的なアプローチで解説しました。

この記事で学んだ内容を実践することで、以下のような効果が期待できます:

  • セキュリティの向上: 適切な認証・認可により、不正アクセスを防げます
  • 開発効率の向上: ミドルウェアによる共通処理により、コードの重複を避けられます
  • 保守性の向上: 明確な責任分離により、コードの保守が容易になります
  • ユーザーエクスペリエンスの向上: 適切なエラーハンドリングにより、ユーザーに分かりやすいフィードバックを提供できます

認証システムは、アプリケーションの信頼性を左右する重要な要素です。今回学んだ内容を基に、あなたのプロジェクトに適した認証システムを構築してください。

セキュリティは完璧ではありませんが、適切な対策を講じることで、リスクを大幅に軽減できます。継続的な学習と改善により、より安全で使いやすいアプリケーションを作り上げていきましょう。

関連リンク