SvelteKit認証ベストプラクティス

SvelteKitは、シンプルさと明示性を重視した設計思想を持つフレームワークです。このページでは、その設計思想に沿った認証・認可の実装パターンとベストプラクティスを、他のフレームワークとの比較を交えながら解説します。

SvelteKitの設計思想

SvelteKitが他のフレームワークと異なる最大の特徴は、明示的で予測可能なコードを重視する点です。

なぜSvelteKitには宣言的ガードがないのか?

多くの開発者が最初に疑問に思うのは、「なぜAngularのRoute GuardsやNestJSの@UseGuardsのような宣言的な認証機能がないのか」という点です。

これは意図的な設計選択です。

  1. 透明性 - コードの実行フローが明確で追跡しやすい
  2. 柔軟性 - 特定のパターンに縛られない自由な実装
  3. 学習曲線 - 特別な概念やAPIを覚える必要がない
  4. Web標準 - 標準的なJavaScript/TypeScriptの知識で理解可能
Rich Harrisの見解

SvelteKitの作者Rich Harrisは、「魔法のような暗黙的な動作よりも、明示的で理解しやすいコードを好む」と述べています。これがSvelteKit全体の設計に反映されています。詳細は GitHub Issue #2326 での議論を参照してください。

推奨3層アーキテクチャ

SvelteKitでの認証実装は、以下の3層構造で整理することを推奨します。

ダイアグラムを読み込み中...

各層の役割と責任

ファイル責任範囲主な処理内容
第1層
共通処理
hooks.server.ts全リクエスト共通• セッション復元
• ユーザー情報取得
• グローバルな前処理
• CSRF対策
第2層
グループ保護
(group)/+layout.server.tsルートグループ全体• 認証要否の判定
• ロールベース制御
• グループ共通データ
• リダイレクト処理
第3層
ページ固有
+page.server.ts個別ページ• 詳細な権限チェック
• リソース所有者確認
• ビジネスロジック
• データ操作

処理フロー図

各層がどのように連携して動作するかを視覚的に示した図です。リクエストは上から下へ流れ、各層で段階的に認証・認可チェックが行われます。右側の要素は各層が利用する外部リソースを表しています。

ダイアグラムを読み込み中...

実装パターン

ここからは、3層アーキテクチャの各層における具体的な実装方法を、実際のコード例とともに解説します。各コードには詳細なコメントを付けているので、実装の意図を理解しながら学習できます。

1. hooks.server.ts - 基盤処理層

SvelteKitのhooks.server.tsは、すべてのリクエストが最初に通過する場所です。ここで共通の認証処理を実装することで、後続のすべての処理でevent.localsを通じてユーザー情報にアクセスできるようになります。これは、Express.jsのミドルウェアに相当する機能です。

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { getSession } from '$lib/server/auth';

// handleハンドラーは全てのHTTPリクエストで実行される
// eventオブジェクトにはリクエスト情報が含まれ、resolveは次の処理への橋渡し
export const handle: Handle = async ({ event, resolve }) => {
  // クッキーからセッションIDを取得(HTTPOnlyで安全)
  const sessionId = event.cookies.get('session');
  
  if (sessionId) {
    // データベースまたはRedisからセッション情報を取得
    const session = await getSession(sessionId);
    
    // セッションが有効期限内の場合のみユーザー情報を設定
    if (session && session.expiresAt > new Date()) {
      // event.localsはリクエスト全体で共有される
      // TypeScriptの型定義はapp.d.tsで行う
      event.locals.user = {
        id: session.userId,
        email: session.user.email,
        roles: session.user.roles // ロールベースアクセス制御用
      };
      event.locals.session = session;
    }
  }
  
  // 次のハンドラーまたはページに処理を委譲
  const response = await resolve(event);
  
  // レスポンスにセキュリティヘッダーを追加(XSS、クリックジャッキング対策)
  response.headers.set('X-Frame-Options', 'DENY'); // iframe埋め込み禁止
  response.headers.set('X-Content-Type-Options', 'nosniff'); // MIMEタイプ推測防止
  response.headers.set('X-XSS-Protection', '1; mode=block'); // XSS対策
  
  return response;
};
typescript

2. ルートグループ - 領域別制御

SvelteKitのルートグループ機能(括弧付きディレクトリ)を使用すると、URLに影響を与えずにページをグループ化できます。これにより、グループごとに異なる認証ロジックやレイアウトを適用できます。例えば、(auth)グループには認証が必要なページを、(public)グループには誰でもアクセスできるページを配置します。

// src/routes/(auth)/+layout.server.ts
// このファイルは(auth)グループ内のすべてのページで実行される
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals, url }) => {
  // hooks.server.tsで設定されたlocals.userをチェック
  if (!locals.user) {
    // 303 See Otherステータスで適切にリダイレクト
    // fromパラメータで元のURLを保存(ログイン後に戻るため)
    throw redirect(303, `/login?from=${encodeURIComponent(url.pathname)}`);
  }
  
  return {
    user: locals.user
  };
};
typescript
// src/routes/(admin)/+layout.server.ts
// 管理者専用セクションのレイアウト
import { error } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals, parent }) => {
  // parent()を呼ぶことで、親レイアウトのload関数を実行
  // これにより、認証チェックの重複を避ける
  await parent();
  
  // ロールベースアクセス制御(RBAC)の実装
  // rolesは配列として管理し、複数ロールに対応
  if (!locals.user?.roles?.includes('admin')) {
    // 403 Forbiddenエラーを返す(認証済みだが権限不足)
    throw error(403, 'Administrator access required');
  }
  
  return {
    isAdmin: true
  };
};
typescript

3. ヘルパー関数 - DRY原則

同じ認証ロジックを複数の場所で繰り返し書くのは、保守性の観点から避けるべきです。SvelteKitでは、認証チェックを関数として抽出し、必要な場所でインポートして使用することで、DRY(Don't Repeat Yourself)原則を実践できます。

// src/lib/server/auth-helpers.ts
import { error, redirect } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';

/**
 * 認証を要求するヘルパー関数
 * @param event - SvelteKitのRequestEventオブジェクト
 * @returns 認証済みユーザー情報
 * @throws リダイレクト(未認証の場合)
 */
export function requireAuth(event: RequestEvent) {
  if (!event.locals.user) {
    // ログインページにリダイレクトし、元のパスを保存
    throw redirect(303, `/login?from=${encodeURIComponent(event.url.pathname)}`);
  }
  return event.locals.user;
}

/**
 * 特定のロールを要求するヘルパー関数
 * @param event - SvelteKitのRequestEventオブジェクト
 * @param role - 必要なロール名('admin', 'editor'等)
 * @returns 認証済みユーザー情報
 * @throws エラー(権限不足の場合)
 */
export function requireRole(event: RequestEvent, role: string) {
  // まず認証チェックを行う(関数の合成)
  const user = requireAuth(event);
  
  if (!user.roles?.includes(role)) {
    // 403 Forbidden - 認証済みだが権限不足
    throw error(403, `Role '${role}' required`);
  }
  
  return user;
}

/**
 * リソースの所有者確認
 * @param event - SvelteKitのRequestEventオブジェクト
 * @param resourceOwnerId - リソースの所有者ID
 * @returns 認証済みユーザー情報
 * @throws エラー(所有者でない場合)
 * @description 自分のリソースのみ操作可能にする(管理者は例外)
 */
export function requireOwnership(
  event: RequestEvent,
  resourceOwnerId: string
) {
  const user = requireAuth(event);
  
  // 所有者本人または管理者のみアクセス可能
  if (user.id !== resourceOwnerId && !user.roles?.includes('admin')) {
    throw error(403, 'Access denied');
  }
  
  return user;
}
typescript

使用例

上記のヘルパー関数を実際のAPIエンドポイントで使用する例です。この方法により、認証ロジックを簡潔に記述でき、コードの可読性と保守性が向上します。

// src/routes/api/posts/[id]/+server.ts
import { requireOwnership } from '$lib/server/auth-helpers';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const DELETE: RequestHandler = async ({ params, locals, url }) => {
  // まず対象の投稿を取得
  const post = await getPost(params.id);
  
  if (!post) {
    throw error(404, 'Post not found');
  }
  
  // 所有者チェック - ヘルパー関数を使用
  // 投稿の作者または管理者のみ削除可能
  requireOwnership({ locals, url }, post.authorId);
  
  // 実際の削除処理
  await deletePost(params.id);
  
  // 成功レスポンス
  return json({ success: true, message: 'Post deleted successfully' });
};
typescript

他フレームワークとの比較

SvelteKitの認証アプローチは他のフレームワークと大きく異なります。以下の図は、各フレームワークでの認証フローの違いを視覚的に示しています。SvelteKitは明示的なチェックを重視し、Angular/NestJSは宣言的なガードシステムを採用しています。

ダイアグラムを読み込み中...

比較表

主要なWebフレームワークとの認証実装の違いを詳細に比較します。各フレームワークには独自の哲学があり、SvelteKitは「シンプルさと明示性」を最優先にしています。

観点SvelteKitAngularNestJSVue Router
アプローチ命令的・明示的宣言的・設定ベース宣言的・デコレータ宣言的・設定ベース
実装場所load関数内Route Guards@UseGuardsNavigation Guards
DIなし(不要)ありありなし
学習コスト低(標準JS)中(Guard概念)高(DI+デコレータ)中(Guard概念)
柔軟性
型安全性高(TypeScript)

Angular開発者への移行ガイド

AngularのRoute Guardsに慣れた開発者向けに、SvelteKitで同様のパターンを実装する方法を示します。SvelteKitにはビルトインのGuardシステムはありませんが、同等の機能を簡単に実装できます。

// Angular風のガード実装(SvelteKit版)
// src/lib/server/guards.ts
import type { RequestEvent } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';

// ガード関数の型定義(AngularのCanActivateに相当)
type GuardFunction = (event: RequestEvent) => boolean | Promise<boolean>;

/**
 * ガードファクトリー関数
 * Angularのガードシステムを模倣した実装
 */
export function createGuard(guardFn: GuardFunction) {
  return async (event: RequestEvent) => {
    const canActivate = await guardFn(event);
    if (!canActivate) {
      // Angularと同様、false返却時はアクセス拒否
      throw error(403, 'Access denied');
    }
  };
}

// 使用例:Angularスタイルのガード定義
// CanActivateに相当
const isAuthenticated = createGuard((event) => !!event.locals.user);

// CanActivateChildに相当
const isAdmin = createGuard((event) => 
  event.locals.user?.roles?.includes('admin') ?? false
);

// 複数条件の組み合わせも可能
const isEditorOrAdmin = createGuard((event) => {
  const roles = event.locals.user?.roles ?? [];
  return roles.includes('editor') || roles.includes('admin');
});

// +page.server.ts内で使用
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async (event) => {
  // Angularのroute configでguardsを設定するのと同等
  await isAuthenticated(event); // 認証チェック
  await isAdmin(event); // 権限チェック
  
  // ガードを通過した後のページロジック
  const data = await fetchAdminData();
  return { data };
};
typescript

NestJS開発者への移行ガイド

NestJSのデコレータベースのガードシステムに慣れた開発者向けに、SvelteKitで同様のパターンを実装する方法を示します。NestJSの@UseGuardsデコレータと同じように、複数のガードを連鎖して適用できます。

// NestJS風のガード合成(SvelteKit版)
// src/lib/server/guards.ts

/**
 * 複数のガードを順番に実行するヘルパー
 * NestJSの@UseGuardsデコレータと同等の機能
 */
export function useGuards(...guards: GuardFunction[]) {
  return async (event: RequestEvent) => {
    // 各ガードを順番に実行(一つでも失敗すればエラー)
    for (const guard of guards) {
      await guard(event);
    }
  };
}

// 使用例:NestJSスタイルのガード適用
// 認証チェックと権限チェックを連鎖
const protectedRoute = useGuards(isAuthenticated, isAdmin);

export const load: PageServerLoad = async (event) => {
  // NestJSの@UseGuards(AuthGuard, RolesGuard)と同等
  await protectedRoute(event);
  
  // ガード通過後のビジネスロジック
  const data = await fetchProtectedData();
  return { data };
};
typescript

プロジェクト規模別アプローチ

プロジェクトの規模に応じて、適切な認証アーキテクチャを選択することが重要です。過度に複雑な実装は開発速度を落とし、シンプルすぎる実装は後々の拡張を困難にします。

小規模プロジェクト(〜10ページ)

個人ブログや小規模な管理ツールなど、シンプルな認証が十分なケースです。各ページで直接認証チェックを実装し、必要に応じて少しずつ抽象化していきます。

// 各+page.server.tsで直接チェック
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
  // シンプルな認証チェック
  if (!locals.user) {
    throw redirect(303, '/login');
  }
  
  // 認証後のデータ取得
  const data = await fetchUserData(locals.user.id);
  return { data };
};
typescript

中規模プロジェクト(10〜50ページ)

企業サイトやSaaSの初期版など、複数のユーザーロールが必要なケースです。ルートグループを使って認証境界を明確にし、ヘルパー関数で共通ロジックを管理します。

routes/
├── (public)/       # 認証不要(ランディングページ、ドキュメント等)
│   ├── +layout.svelte
│   ├── about/
│   └── docs/
├── (auth)/         # 認証必須(一般ユーザー向け)
│   ├── +layout.server.ts  # 認証チェック
│   ├── dashboard/
│   └── profile/
└── (admin)/        # 管理者のみ
    ├── +layout.server.ts  # 権限チェック
    ├── users/
    └── settings/
null

大規模プロジェクト(50ページ〜)

エンタープライズアプリケーションや大規模SaaSなど、複雑な権限管理が必要なケースです。ポリシーベースの認可システムを実装し、ビジネスロジックと認可ロジックを分離します。

// src/lib/server/auth/policies.ts
// ポリシーベースの認可システム

/**
 * 認可ポリシークラス
 * ビジネスルールを中央集権的に管理
 */
export class AuthPolicy {
  /**
   * 投稿の閲覧権限チェック
   */
  static async canViewPost(user: User, post: Post): Promise<boolean> {
    // 公開投稿、作者本人、またはモデレーターは閲覧可能
    return post.isPublic || 
           post.authorId === user.id || 
           user.roles.includes('moderator');
  }
  
  /**
   * 投稿の編集権限チェック
   */
  static async canEditPost(user: User, post: Post): Promise<boolean> {
    // 作者本人または管理者のみ編集可能
    return post.authorId === user.id || 
           user.roles.includes('admin');
  }
  
  /**
   * ユーザー管理権限チェック
   */
  static async canManageUsers(user: User): Promise<boolean> {
    return user.roles.includes('admin') || 
           user.roles.includes('user-manager');
  }
}

// 使用例:ポリシーベースの認可
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params, locals, url }) => {
  // 認証チェック
  const user = requireAuth({ locals, url });
  
  // データ取得
  const post = await getPost(params.id);
  if (!post) {
    throw error(404, 'Post not found');
  }
  
  // ポリシーベースの権限チェック
  if (!await AuthPolicy.canViewPost(user, post)) {
    throw error(403, 'Cannot view this post');
  }
  
  // 編集権限も同時にチェックしてUIで活用
  const canEdit = await AuthPolicy.canEditPost(user, post);
  
  return { 
    post,
    canEdit // UIで編集ボタンの表示/非表示に使用
  };
};
typescript

アンチパターン

認証システムの実装でよくある間違いと、それらを避けるための推奨パターンを紹介します。セキュリティと保守性の観点から、これらのアンチパターンを避けることが重要です。

避けるべきパターン

1. クライアントサイドのみの認証チェック

クライアントサイドのJavaScriptは簡単に回避できるため、セキュリティ上の保護にはなりません。必ずサーバーサイドで検証しましょう。

// ❌ 悪い例:クライアントサイドのみ
// +page.svelte
<script>
  import { user } from '$lib/stores';
  import { goto } from '$app/navigation';
  
  // ブラウザのDevToolsで簡単に回避可能
  if (!$user) goto('/login');
</script>
javascript

2. load関数での認証チェック漏れ

各ページで個別に認証チェックを実装していると、どこかで必ず漏れが発生します。ルートグループを使って統一的に管理しましょう。

// ❌ 悪い例:認証チェック忘れ
// src/routes/admin/users/+page.server.ts
export const load: PageServerLoad = async ({ locals }) => {
  // 認証チェックなしで機密データを返している!
  const users = await getAllUsers();
  const secrets = await getSystemSecrets();
  return { users, secrets };
};
typescript

3. 過度に複雑な認証ロジック

複雑な条件分岐を各ページに直接書くと、保守が困難になり、バグの温床になります。ポリシークラスやヘルパー関数に抽出しましょう。

// ❌ 悪い例:読みにくく、テストしにくい条件分岐
// src/routes/posts/[id]/edit/+page.server.ts
export const load: PageServerLoad = async ({ params, locals }) => {
  const post = await getPost(params.id);
  
  // 複雑すぎる条件分岐(何をチェックしているか不明瞭)
  if (user && user.roles && (user.roles.includes('admin') || 
      (user.roles.includes('moderator') && post.status === 'draft') ||
      (user.id === post.authorId && post.status !== 'deleted'))) {
    return { post };
  }
  throw error(403);
}
typescript

推奨パターン

1. サーバーサイドでの確実な検証

// ✅ 良い例:サーバーサイドでの検証
// src/routes/admin/+page.server.ts
import type { PageServerLoad } from './$types';
import { requireRole } from '$lib/server/auth-helpers';

export const load: PageServerLoad = async (event) => {
  // サーバーサイドで確実に検証
  requireRole(event, 'admin');
  
  const data = await getAdminData();
  return { data };
};
typescript

2. ヘルパー関数による共通化

// ✅ 良い例:ロジックをヘルパー関数に抽出
// src/lib/server/auth/post-permissions.ts
export function canEditPost(user: User, post: Post): boolean {
  // 複雑なロジックをわかりやすい関数に
  if (user.roles.includes('admin')) return true;
  if (user.id === post.authorId && post.status !== 'deleted') return true;
  if (user.roles.includes('moderator') && post.status === 'draft') return true;
  return false;
}
typescript

3. 明確な責任分離

// ✅ 良い例:各層の責任を明確に
// hooks.server.ts - セッション管理
// (auth)/+layout.server.ts - 認証チェック
// +page.server.ts - ビジネスロジック
typescript

ベストプラクティスチェックリスト

認証システムを実装する際に確認すべき項目をリストアップしました。このチェックリストを活用して、セキュアで保守しやすい認証システムを構築してください。

基本設計

  • hooks.server.tsでセッション管理を実装
  • ルートグループで認証境界を明確化
  • ヘルパー関数でDRY原則を遵守
  • TypeScriptで型安全性を確保

セキュリティ

  • すべての認証チェックをサーバーサイドで実施
  • セッションの有効期限を適切に設定
  • CSRF対策(SvelteKitのデフォルト機能を活用)
  • XSS対策(HTTPOnlyクッキーの使用)

エラーハンドリング

  • 統一されたエラーレスポンス形式
  • 適切なHTTPステータスコードの使用
  • ユーザーフレンドリーなエラーメッセージ

パフォーマンス

  • 不要な認証チェックの削減
  • セッション情報のキャッシング
  • 並列データ取得の活用

保守性

  • 認証ロジックの集約化
  • テスト可能な設計
  • ドキュメント化

関連リソース

SvelteKitの認証実装に関する追加情報や参考資料を以下にまとめました。

公式ドキュメント

コミュニティの議論

SvelteKitにRoute Guardsのような宣言的な認証機能がない理由や、現在のアプローチが選ばれた背景について、コミュニティで活発な議論が行われています。

主要なGitHub Discussions

Rich Harris(Svelte作者)の見解

Rich Harrisは、SvelteKitが明示的なアプローチを選んだ理由について、以下のように説明しています。

"We prefer explicit over implicit. Hooks provide a more flexible solution, and declarative guards often become limiting."

詳細は Issue #2326のコメント で確認できます。

認証ライブラリ

実装例とチュートリアル

セキュリティリソース

公式ドキュメント

GitHubの議論

実装例

コミュニティリソース

まとめ

SvelteKitの認証実装は、一見すると他のフレームワークより「原始的」に見えるかもしれません。しかし、これは意図的にシンプルで明示的な設計を選択した結果です。

この設計により

  • コードの流れが追跡しやすい
  • デバッグが容易
  • 特殊な概念を学ぶ必要がない
  • 柔軟な実装が可能

最初は戸惑うかもしれませんが、慣れれば非常に生産的で保守しやすいコードが書けるようになります。重要なのは、SvelteKitの設計思想を理解し、それに沿った実装を心がけることです。

移行のコツ

他のフレームワークから移行する際は、無理に以前のパターンを再現しようとせず、SvelteKitの流儀に従うことをお勧めします。シンプルさを受け入れることで、より良いコードが書けるようになります。

Last update at: 2025/09/08 20:05:16