SvelteKitで認証・セッション管理を実装した話

TODOアプリにログイン機能を追加しました。 SvelteKit独自の仕組みである hooks.server.ts と locals を使い、ライブラリなしでCookieベースの認証を実装しています。

このシリーズの記事

  1. SvelteKitで記事一覧・詳細ページを作った話
  2. SvelteKitでTODOアプリを作った話
  3. SvelteKitで認証・セッション管理を実装した話(この記事)
  4. SvelteKitをCloudflare Pagesにデプロイした話

作ったもの

  • /login :パスワードでログインしてCookieをセット
  • /todo :未ログイン時は /login にリダイレクト
  • ログアウト:Cookieを削除して /login に戻る

ファイル構成

src/
  hooks.server.ts           # 全リクエストの共通処理
  app.d.ts                  # locals の型定義
  routes/
    login/
      +page.svelte          # ログインページ(UI)
      +page.server.ts       # ログイン処理(actions)
    todo/
      +page.svelte          # TODOページ(UI)
      +page.server.ts       # 保護ルート・ログアウト

リクエストの流れ

すべてのリクエストは hooks.server.ts の handle 関数を通ってからページ処理に入ります。

ブラウザ → handle() → resolve() → load() / actions() → レスポンス

resolve() を呼ぶ前が前処理(認証チェックなど)、呼んだ後が後処理(ヘッダー追加など)のタイミングです。

hooks.server.ts

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

export const handle: Handle = async ({ event, resolve }) => {
  const sessionId = event.cookies.get('session_id');

  if (sessionId === 'logged-in') {
    event.locals.user = { name: 'Jiru' };
  } else {
    event.locals.user = null;
  }

  return resolve(event);
};

event.cookies でCookieを読み、event.locals にユーザー情報をセットします。locals にセットした値はそのリクエスト内のどのファイル(load 関数・actions)からも参照できます。

locals の型定義

event.locals にセットする値の型は src/app.d.ts で定義します。

// src/app.d.ts
declare global {
  namespace App {
    interface Locals {
      user: { name: string } | null;
    }
  }
}

export {};

ここに書かないと TypeScript が locals.user を認識しません。

ログイン処理

// src/routes/login/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions: Actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const password = data.get('password') as string;

    if (password === '1234') {
      cookies.set('session_id', 'logged-in', {
        path: '/',
        httpOnly: true,
        maxAge: 60 * 60 * 24,
      });
      redirect(303, '/todo');
    }
  },
};

アクション名を default にするとフォームに action="?/default" の指定が不要になります。ページ内にフォームが1つだけの場合に使います。

cookies.set のオプションはこちらです:

オプション 内容
path Cookieを送るパスのスコープ(/ で全ページ)
httpOnly JavaScriptからCookieを読めなくする(XSS対策)
maxAge Cookieの有効期限(秒)。ここでは1日

保護ルート

// src/routes/todo/+page.server.ts(抜粋)
export const load: PageServerLoad = ({ locals }) => {
  if (!locals.user) {
    redirect(303, '/login');
  }

  return { todos, user: locals.user };
};

load 関数の引数 locals に hooks.server.ts でセットした値が入っています。未ログインの場合はそのまま /login に飛ばします。

ログアウト処理

// src/routes/todo/+page.server.ts(actionsに追加)
  logout: async ({ cookies }) => {
    cookies.delete('session_id', { path: '/' });
    redirect(303, '/login');
  },

cookies.delete でCookieを削除するだけです。path は cookies.set と合わせる必要があります。

actions で使えるプロパティ

actions の各関数が受け取る RequestEvent の主なプロパティです。load 関数でも同じものが使えます。

プロパティ 内容
request フォームデータやリクエストボディの取得
cookies Cookieの読み書き・削除
locals hooks.server.ts でセットしたデータ
params URLのパラメータ([id] など)
url URLオブジェクト(クエリパラメータなど)
fetch 認証情報付きのサーバー側fetch

まとめ

概念 内容
hooks.server.ts 全リクエストに共通処理を挟むファイル
handle 関数 resolve() の前後で前処理・後処理を書く
event.locals リクエスト内でデータを伝搬する入れ物
app.d.ts の Locals locals の型定義
保護ルート load 関数で locals を確認してリダイレクト
default アクション フォームが1つだけのページで使うアクション名

hooks.server.ts を使うと、認証チェックをページごとに書かずに一箇所にまとめられます。より本格的な実装では Lucia などのライブラリと組み合わせるのが一般的です。


前の記事:SvelteKitでTODOアプリを作った話

次の記事:SvelteKitをCloudflare Pagesにデプロイした話

← トップページに戻る