Lucia、PlanetScale、Upstash Redis を使用した SvelteKit での安全でタイプセーフな認証
Upstash ブログの前回のガイドがBytes ニュースレターに掲載されました。 、SvelteKit パーティーを続けようと思いました。
Svelte の熱烈なファンとして、私は日に日に参加者が増えているのを目にして、将来がとても楽しみです。
まだ目立たないツールの 1 つが Lucia です。
このガイドでは、Lucia で認証を起動して実行する方法を説明します。データベースのニーズには PlanetScale を使用し、セッションの処理には Upstash Redis を使用します。
以下は、このガイドの最終目標のスクリーンショットです。サンプル リポジトリはここにあります。

このガイドは SvelteKit で提供されますが、Lucia はあらゆるフレームワークをサポートしているため、このガイドのほとんどは一般的なフレームワークに簡単に適用できます。
ルシアとは何ですか?
簡単に言えば、Lucia はユーザーとセッションの処理を簡単にする TypeScript 用のライブラリです。もともとこのライブラリは SvelteKit 用に作成されましたが、継続的に進化し、現在ではほぼすべてのフレームワークで適切に動作するのに十分な多用途性を備えています。
Lucia の優れている点は、ユーザー エクスペリエンスを犠牲にすることなく、複雑な認証を管理するために必要なすべてを備えていることです。Lucia をプリミティブのセットとして考えてください。コードをどのように構成し、ユーザー エクスペリエンスを処理するかはあなた次第です。Lucia には、理解することが重要な重要な部分がいくつかあります。
ミドルウェア これにより、Lucia はさまざまなフレームワークやランタイムのリクエストとレスポンスを読み取ることができます。
以下は、ミドルウェアを構成する方法の例です。
import { lucia } from "lucia";
import { node } from "lucia/middleware";
// import { nextjs } from "lucia/middleware";
// import { h3 } from "lucia/middleware";
export const auth = lucia({
env: "DEV", // "PROD" if deployed to HTTPS
middleware: node(),
}); データベース アダプター Lucia がユーザーとセッションを保存および取得できるようにします。アダプターを提供することで、Lucia はこれらのタイプをクエリする方法を認識します。アダプターには 2 種類あります。通常のアダプターとセッションアダプター。この特定のガイドでは、PlanetScale でホストされている mySQL データベースを使用してユーザーを保存し、Upstash でホストされている Redis インスタンスを使用してセッションを処理します。
以下に、データベース アダプタの設定方法の例を示します。
import { prisma } from "@lucia-auth/adapter-prisma";
import { PrismaClient } from "@prisma/client";
import { lucia } from "lucia";
const client = new PrismaClient();
const auth = lucia({
env: "DEV",
adapter: prisma(client),
}); 背景情報を踏まえた上で、早速本題に入りましょう。
前提条件
アプリを立ち上げて実行し、手順に従って進むには、次のものが必要です。
- SvelteKit の基本的な理解。フォームとルーティングを処理できると有利です。
- Drizzle ORM に関する基本的な知識
- PlanetScale のアカウントとデータベース。
- Redis インスタンス (Upstash Redis など) へのアクセス
はじめに
効率を高めるため、アプリケーション全体を最初から作成することはありません。
代わりに、sveltekit-lucia-redis のクローンを作成できます。 Upstash サンプル リポジトリのディレクトリを参照してください。
リポジトリをダウンロードした後、cd を使用してアプリケーションに移動します。 コマンドを実行し、好みのパッケージ マネージャー経由で依存関係をインストールし、.env を設定します。 .env.example を複製して変数を作成する .
重要な部分を理解する
ここでは、すべての重要な部分を簡単にまとめます。
src/lib/server/auth/index.ts- ここで Lucia を構成します。src/lib/server/drizzle- Drizzle を使用すると、Drizzle Kit を使用して PlanetScale に簡単にプッシュできる mySQL スキーマを簡単に作成できます。src/lib/server/planetscale- ユーザーを管理するために Lucia アダプター構成で使用する Upstash クライアントをエクスポートします。src/lib/server/upstash- セッションを管理するために Lucia アダプター構成で使用する Upstash クライアントをエクスポートします。
コードの分解
Lucia の設定
最初に行う必要があるのは、Lucia を構成することです。これを行うには、src/lib/server/auth/index.ts で新しいファイルを作成します。 .
import { planetscale } from "@lucia-auth/adapter-mysql";
import { dev } from "$app/environment";
import { lucia } from "lucia";
import { sveltekit } from "lucia/middleware";
import { ps } from "../planetscale";
export enum PROVIDER_ID {
EMAIL = "email",
}
export const auth = lucia({
adapter: {
user: planetscale(ps, {
user: "users",
key: "keys",
/**
* Sessions are handled by Upstash Redis.
*/
session: null,
}),
},
middleware: sveltekit(),
env: dev ? "DEV" : "PROD",
getUserAttributes: (data) => {
return {
userId: data.id,
email: data.email,
};
},
});
export type Auth = typeof auth; ここで何が起こっているかを簡単にまとめます。
lucia をインポートします lucia の関数 Lucia の構成をセットアップするためのパッケージ。
最初に行うことは、adapter を構成することです。 財産。ここで、Lucia にユーザーとセッションの処理方法を指示します。
セッションプロパティを null に設定します。 Redis を使用してセッションを処理したいためです。文字列 'session' を使用する場合 ここでは代わりに、Lucia はユーザーとセッションの両方に同じアダプターを使用します (これらの文字列はデータベース内のテーブルに対応します)。
セッション アダプターについては今のところ心配しないでください。それについては後で説明します。
middleware 内 プロパティを使用して、SvelteKit を使用していることを Lucia に知らせることができます。これにより、Lucia はリクエスト オブジェクトとレスポンス オブジェクトを読み取ることができるようになります。
エクスポートされた Auth をメモします。 タイプ。これは auth のタイプです。 オブジェクト。 SvelteKit ローカルをセットアップするにはこれが必要になります。
Lucia で優れた型推論を実現する
Lucia は TypeScript で書かれているため、すぐに優れた型推論を得ることができます。 SvelteKit が Auth について認識していることを確認しましょう。 先ほど作成したタイプです。
app.d.ts を開きます ファイルに以下を追加します:
import type { Auth as LuciaAuth } from "$lib/server/auth";
import type { AuthRequest, Session, User } from "lucia";
declare global {
namespace App {
// interface Error {}
interface Locals {
auth: AuthRequest;
session: Session | null;
}
interface PageData {
user?: User;
}
// interface Platform {}
}
}
/// <reference types="lucia" />
declare global {
namespace Lucia {
type Auth = LuciaAuth;
type DatabaseUserAttributes = {
email: string;
};
type DatabaseSessionAttributes = {};
}
}
export {};
Auth を追加することで Lucia に入力します 名前空間にアクセスできるようになりました。auth locals からのオブジェクト SvelteKit ルート内のオブジェクト。
ただし、Lucia からインポートされたものはすべて正しいタイプを持つようになります。
これらの型が揃ったので、hooks.server.ts を設定できます。 。ここで AuthRequest をバインドします。 および Session 現在のリクエストに反対します。
これにより、locals を介してサーバー上で簡単にアクセスできるようになります。 .
import type { Handle } from "@sveltejs/kit";
import { sequence } from "@sveltejs/kit/hooks";
import { auth } from "$lib/server";
const auth_handle: Handle = async ({ event, resolve }) => {
event.locals.auth = auth.handleRequest(event);
event.locals.session = await event.locals.auth.validate();
return resolve(event);
};
export const handle = sequence(auth_handle);
sequence もインポートします。 これは、複数のフックを順番に実行できるようにするヘルパー関数です。これは、後でルートを保護するときに役立ちます。
ユーザー モデルの作成
Lucia の設定が完了したので、ユーザー モデルを作成できます。
今はとても暑いので、Drizzle ORM を使用します。

そして彼らのミームは的を射ています。これを見てください。
続行する前に、PlanetScale でデータベースを作成する必要があります。そして、Drizzle 設定ファイルをセットアップします。これにより、Drizzle CLI がデータベースに接続できるようになります。
drizzle.config.tsimport dotenv from "dotenv";
import type { Config } from "drizzle-kit";
dotenv.config();
const username = process.env.DATABASE_USERNAME;
const password = process.env.DATABASE_PASSWORD;
const host = process.env.DATABASE_HOST;
const db = process.env.DATABASE_NAME;
const connectionString = `mysql://${username}:${password}@${host}/${db}?ssl={"rejectUnauthorized":true}`;
export default {
schema: "./src/lib/server/drizzle/schema/index.ts",
driver: "mysql2",
dbCredentials: {
connectionString: connectionString,
},
} satisfies Config; mySQL アダプターを使用しているため、Lucia はユーザー モデルが特定の構造を持つことを期待しています。これに関する詳細については、ドキュメントを参照してください。
次のコードを src/lib/server/drizzle/schema/index.ts に配置します。 .
import { relations } from "drizzle-orm";
import {
bigint,
datetime,
index,
int,
mysqlEnum,
mysqlTable,
timestamp,
unique,
varchar,
} from "drizzle-orm/mysql-core";
export const users = mysqlTable(
"users",
{
id: varchar("id", { length: 255 }).primaryKey(),
createdAt: timestamp("createdAt").defaultNow().onUpdateNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
email: varchar("email", { length: 191 }).notNull(),
},
(table) => {
return {
idIdx: index("users_id_idx").on(table.id),
userIdKey: unique("users_id_key").on(table.id),
};
},
);
export const keys = mysqlTable(
"keys",
{
id: varchar("id", { length: 255 }).primaryKey(),
hashedPassword: varchar("hashed_password", { length: 255 }),
userId: varchar("user_id", { length: 255 }).notNull(),
},
(table) => {
return {
userIdIdx: index("keys_user_id_idx").on(table.userId),
keyIdKey: unique("keys_id_key").on(table.id),
};
},
);
pnpm drizzle-kit push:mysql を実行します スキーマを PlanetScale にプッシュします。
出来上がり!これで、Lucia がユーザーを管理するために使用できるユーザー モデルが完成しました。
セッション管理の設定
ユーザー モデルが完成したので、セッション管理を設定できます。
Upstash Redis を使用してセッションを処理します。ここで無料アカウントにサインアップできます。

ダッシュボードに入ったら、新しいデータベースを作成して環境変数をコピーするだけです。

次に、これらの変数を .env に追加します。 ファイル。そして、以下を src/lib/server/upstash/index.ts に追加します。 .
import { Redis } from "@upstash/redis";
import {
UPSTASH_REDIS_REST_TOKEN,
UPSTASH_REDIS_REST_URL,
} from "$env/static/private";
export const upstashClient = new Redis({
url: UPSTASH_REDIS_REST_URL,
token: UPSTASH_REDIS_REST_TOKEN,
});
ここで、Lucia を設定したときに session を設定したことを思い出してください。 null へ 。これは、セッションの処理に Redis を使用したいためです。現在の構成は次のようになります。
import { planetscale } from "@lucia-auth/adapter-mysql";
import { upstash } from "@lucia-auth/adapter-session-redis";
import { dev } from "$app/environment";
import { lucia } from "lucia";
import { sveltekit } from "lucia/middleware";
import { ps } from "../planetscale";
import { upstashClient } from "../upstash";
export enum PROVIDER_ID {
EMAIL = "email",
}
export const auth = lucia({
adapter: {
user: planetscale(ps, {
user: "users",
key: "keys",
session: null,
}),
// Instruct Lucia to use Upstash Redis for sessions
session: upstash(upstashClient),
},
middleware: sveltekit(),
env: dev ? "DEV" : "PROD",
getUserAttributes: (data) => {
return {
userId: data.id,
email: data.email,
};
},
});
export type Auth = typeof auth; 幸運なことに、Lucia にはすぐに使える Upstash Redis アダプターが付属しています。したがって、必要なのは、それをインポートして Upstash クライアントに渡すことだけです。
これで簡単になりました!
ルートの作成
Lucia の設定が完了したので、ルートを作成できます。
src/routes に次のフォルダーを作成します。 。今のところファイルについては心配しないでください。後で少し説明します。
src/routes/authsrc/routes/auth/signinsrc/routes/auth/signup
src/routes/app
ヒント: 同じフォルダ名またはグループの下に特定の「機能」があると、リダイレクトを実行したりルートを保護したりする際の管理が容易になります。
サインイン ページの作成
ようやく、フロントエンドの作業を行うことができます。サインイン ページから始めましょう。
src/routes/auth/signin/+page.svelte で新しいファイルを作成します 。ここではスタイル設定を省略し、機能に焦点を当てます。ただし、完全なコードはサンプル リポジトリにあります。
<script lang="ts">
import { enhance } from '$app/forms';
import { Button, Input, Label, PasswordInput } from '$lib/components/common';
import type { ActionData } from './$types';
export let form: ActionData;
let loading = false;
let email = '';
let password = '';
</script>
<form
method="POST"
use:enhance={() => {
loading = true;
return async ({ update }) => {
loading = false;
update();
};
}}
>
{#if form && form.error}
<div class="p-2 mb-4 text-sm text-center text-red-900 bg-red-200 rounded-sm">
Error: {form.error}
</div>
{/if}
<div class="grid gap-2.5">
<div class="grid gap-1">
<Label for="email">Email</Label>
<Input
bind:value={email}
name="email"
placeholder="Email"
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
required={true}
/>
</div>
<div class="grid gap-1">
<Label for="password">Password</Label>
<PasswordInput name="password" placeholder="Password" bind:value={password} />
</div>
<div class="mt-6 col-span-full">
<Button type="submit" class="w-full" disabled={loading}>
{#if loading}
Loading...
{:else}
Sign in
{/if}
</Button>
</div>
</div>
</form>
use:enhance アクションはフォームを徐々に強化し、フォームが送信されたときに読み込み状態を表示できるようにします。エンハンスについて詳しくは、こちらをご覧ください。
コードの残りの部分は、一目瞭然です。
サインインリクエストの処理
SvelteKit を使用すると、POST リクエストの処理が驚くほど簡単になります。必要なのは、ファイル +page.server.ts を作成することだけです。 +page.svelte と同じディレクトリ内 ファイルを作成し、actions をエクスポートします。 少なくとも default を持つオブジェクト プロパティ。
import { fail, redirect } from "@sveltejs/kit";
import { auth, PROVIDER_ID } from "$lib/server";
import { LuciaError } from "lucia";
import type { Actions, PageServerLoad } from "./$types";
export const actions = {
/* our actions here */
}; ファイルのこの部分の重要な要素を見てみましょう。
PROVIDER_ID をインポートしました 列挙型と auth src/lib/server/authから 、先ほど作成したものです。この auth オブジェクトには、ユーザーとセッションを管理するために必要なすべてのメソッドが含まれています。
次に、actions を見てみましょう。 オブジェクト。リクエスト オブジェクトからフォーム データを取得し、基本的なハウスキーピングを行うことができます。
actions = {
default: async ({ request, locals }) => {
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const fields = [email, password];
if (fields.some(field => !field)) {
return fail(400, {
error: "All fields are required"
});
} 次に、ユーザーのサインインを試みます。ユーザーが存在しない場合、またはパスワードが間違っている場合、Lucia はエラーをスローします。私たちは、エラーをキャッチし、エラー メッセージとともに 400 レスポンスを返すことで、このイベントに備えました。
try {
const user = await auth.useKey(PROVIDER_ID.EMAIL, email.toLowerCase(), password);
const session = await auth.createSession({
userId: user.userId,
attributes: {}
});
locals.auth.setSession(session);
} catch (err) {
if (
err instanceof LuciaError &&
(err.message === 'AUTH_INVALID_KEY_ID' || err.message === 'AUTH_INVALID_PASSWORD')
) {
return fail(400, {
error: 'Incorrect username of password'
});
}
return fail(400, {
error: 'An unknown error occurred'
});
} ユーザーが存在し、パスワードが正しい場合は、新しいセッションが作成されます。
最後に、ユーザーをダッシュボードにリダイレクトします。
return redirect('/app'); おそらくおわかりのとおり、Lucia は認証の複雑さの多くを抽象化します。私たちがしなければならないのは、適切なメソッドを呼び出すことだけです。残りは Lucia が処理します。

パスワードをハッシュしたり、セッションを作成したり、Cookie を管理したりする必要はありません。ルシアは私たちのためにすべてをやってくれます。しかもすべてタイプセーフです!
サインアップ ページの作成
src/routes/auth/signup/+page.svelte で新しいファイルを作成します .
サインアップ ページはサインイン ページとよく似ているため、ここで説明することはあまりありません。唯一の違いは、パスワードの確認を求めていることです。
src/routes/auth/signup/+page.svelte<script lang="ts">
import { enhance } from '$app/forms';
import { Button, Input, Label, PasswordInput } from '$lib/components/common';
import type { ActionData } from './$types';
export let form: ActionData;
let loading = false;
let email = '';
let password = '';
let passwordConfirmation = '';
</script>
<form
method="POST"
use:enhance={() => {
loading = true;
return async ({ update }) => {
loading = false;
update();
};
}}
>
{#if form && form.error}
<div class="p-2 mb-4 text-sm text-center text-red-900 bg-red-200 rounded-sm">
Error: {form.error}
</div>
{/if}
<div class="grid gap-2.5">
<div class="grid gap-1">
<Label for="email">Email</Label>
<Input
bind:value={email}
name="email"
placeholder="Email"
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
required={true}
/>
</div>
<div class="grid gap-1">
<Label for="password">Password</Label>
<PasswordInput name="password" placeholder="Password" bind:value={password} />
</div>
<div class="grid gap-1">
<Label for="passwordConfirmation">Confirm password</Label>
<PasswordInput
name="passwordConfirmation"
placeholder="Repeat password"
bind:value={passwordConfirmation}
/>
</div>
<div class="mt-6 col-span-full">
<Button type="submit" class="w-full" disabled={loading}>
{#if loading}
Loading...
{:else}
Sign in
{/if}
</Button>
</div>
</div>
</form> サインアップリクエストの処理
src/routes/auth/signup/+page.server.ts で新しいファイルを作成します .
サインアップでは、サインアップ フォームと同様のパターンを使用する必要がありますが、フォームからのデータ取得、フィールドの検証からユーザーの作成と既存のユーザーの処理に至るまで、ほとんど違いはありません。
インポートはサインイン ページの場合と同じなので、その部分は省略します。
まずリクエスト オブジェクトからフォーム データを取得し、基本的なハウスキーピングを行います。パスワードが一致するかどうかも確認します。
const formData = await request.formData();
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const passwordConfirmation = formData.get('passwordConfirmation') as string;
const fields = [email, password, passwordConfirmation];
if (fields.some((field) => !field)) {
return fail(400, {
error: 'All fields are required'
});
}
if (password !== passwordConfirmation) {
return fail(400, {
error: 'Passwords do not match'
});
} 次に、Drizzle ORM を使用して電子メールでユーザーを検索してみます。ユーザーが存在する場合、エラー メッセージとともに 400 レスポンスが返されます。
try {
const user = await db.query.users.findFirst({
where: eq(schema.users.email, email.toLowerCase()),
});
if (user) {
return fail(400, {
error: "User with this email already exists"
});
} ユーザーが存在しない場合は、Lucia を使用して新しいユーザーを作成します。
const newUser = await auth.createUser({
key: {
providerId: PROVIDER_ID.EMAIL,
providerUserId: email.toLowerCase(),
password: password,
},
attributes: {
email,
},
}); 最後に、新しいセッションを作成し、ユーザーをダッシュボードにリダイレクトします。
const session = await auth.createSession({
userId: newUser.userId,
attributes: {},
});
locals.auth.setSession(session); 簡単ピージーレモン絞り!
ボーナス:サインアウト ページの作成
その一方で、ユーザーをサインアウトするためのエンドポイントを作成しましょう。 src/routes/auth/signout/+server.ts で新しいファイルを作成します .
そして、次のコードを追加します。
src/routes/auth/signout/+server.tsimport { auth } from "$lib/server";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals }) => {
const session = await locals.auth.validate();
if (!session) {
return new Response(null, {
status: 400,
});
}
// Invalidate session or alternatively, you can delete all sessions: await auth.invalidateAllUserSessions(session.userId);
await auth.invalidateSession(session.sessionId);
// Remove the cookie.
locals.auth.setSession(null);
return new Response(null, {
status: 200,
});
}; ここでも、Lucia はセッションを非常に簡単に無効にすることで私たちをサポートします。ここで行う必要があるのは、ユーザーがサインアウト ボタンをクリックしたときにこのエンドポイントを呼び出すことだけです。
src/routes/app/+page.svelte<script lang="ts">
import { goto } from '$app/navigation';
import { Button } from '$lib/components/common';
async function handleSignOut() {
const response = await fetch('/auth/signout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
goto('/auth/signin', {
replaceState: true,
invalidateAll: true
});
}
}
</script>
<Button on:click={handleSignOut}>Sign out</Button> これで終わりです
Lucia、PlanetScale、Upstash Redis を使用してタイプセーフ認証を作成することに成功しました。そして、私たちはルシアができることのほんの表面をなぞっただけです。
繰り返しになりますが、このガイドのリポジトリはここにあります。Lucia について詳しく知りたい場合は、ドキュメントをチェックすることを強くお勧めします。
次に進む前に、Upstash Discord コミュニティに遊びに来てください。楽しい時間を過ごしています。また、SvelteKit のコンテンツをもっと知りたい場合は、私のブログのこちらから見つけることができます。
-
RedisDaysLondon2022の概要
3部構成の世界規模のバーチャルイベントであるRedisDaysは、今年ロンドンに立ち寄り、Redisの専門家と尊敬されるゲストがミリ秒未満の速度の力を深く掘り下げました。 RedisDaysは、リアルタイムデータの新製品の発表、ベストプラクティス、新しい顧客のユースケースとともに、過去1年間に行ったすべての学習と技術的進歩をコミュニティと共有する機会です。以下は、RedisDaysLondon2022のハイライトの一部です。 基調講演:リアルタイムの時代は今です。加速するか混乱する Redisの共同創設者兼CEOであるOferBengalは、基調講演で物事を開始しました。基調講演は、1
-
Redis HSTRLEN –ハッシュに含まれるフィールド値の長さを取得する方法
このチュートリアルでは、キーに格納されているハッシュ値に含まれるフィールドの値の長さを取得する方法について学習します。このために、Redis HSTRLENを使用します コマンド。 HSTRLENコマンド このコマンドは、指定されたキーに格納されているハッシュ値のフィールドに関連付けられている値の長さ(文字数)を返します。キーが存在しない場合、またはキーは存在するがハッシュ値に指定されたフィールドが含まれていない場合はOが返され、キーは存在するがキーに格納されている値がハッシュデータ型ではない場合はエラーが返されます。 RedisHSTRLENコマンドの構文は次のとおりです。- 構文: