【③ セッション】SvelteKit on Cloudflareでお問い合わせフォームをつくる
私たちは都内を中心に活動しているアマチュアオーケストラの Orchestra Canvas Tokyo です。
弊団のホームページ、ブログのリファクタリングにおいてできた、お問い合わせフォーム実装に関する知見をまとめた本シリーズ。
今回は SvelteKit x Cloudflare KV でセッションを実装します!
このシリーズの記事一覧
- ① サイト作成:SvelteKit x Cloudflare Pages
- ② フォーム作成:SvelteKit x Zod x Google reCAPTCHA v3
- ③ セッション:SvelteKit x Cloudflare KV ← 今回の記事
- ④ データベース:SvelteKit x Cloudflare D1
- ⑤ メール送信:SvelteKit x Resend
このシリーズで作成したお問い合わせフォームはこちら。
はじめに
お問い合わせフォームには、セッションを用意することが望ましいです。
CSRF 対策のためです。
IPA のガイドを参考に、CSRF トークンを発行し、検証するように実装します。
参考:安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ) | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構
技術選定
今回は、シンプルなセッション機能があれば十分です。
svelte-kit-sessions パッケージを用いることとします。
このパッケージは、Cloudflare が提供する Key-Value ストレージである Cloudflare KV をサポートする(サブ)パッケージも開発されています。
Cloudflare Pages x KV
Cloudflare Pages からも Pages Functions という機能を用いて Cloudflare KV などを呼び出すことができます。
Cloudflare Workers で Cloudflare KV を呼び出すのと似たイメージです。
やや設定は異なるようですが、「Cloudflare Workers でできることは Pages でもできる」ということです。
これは大変便利ですね!
費用
Cloudflare KV の無料枠は次の通りです。
参考︰Pricing - Cloudflare Workers KV
- 読み取り上限: 100,000 回/日
- 書き込み、削除、リスト上限: 1,000 回/日
- 容量上限: 1GB
Cloudflare Pages Functions は Workers と枠が同様です。無料枠は次の通りです。
参考:Pricing - Cloudflare Workers
- リクエスト上限: 100,000 件/日
- CPU 時間上限: 10ms/呼び出し
今回のような、シンプルかつ頻用されないアプリケーションであれば、問題なく使える水準ではないでしょうか。
環境構築
依存関係のインストール
まずは依存関係をインストールします。
npm i svelte-kit-sessions svelte-kit-connect-cloudflare-kv
Wrangler の設定
Cloudflare Pages Functions の設定は Wrangler という CLI ツールを用いて行います。
これも Workers と一緒です。
グローバルインストールしてもいいですが、ここではnpx
コマンドで実行します。
(好みの問題です)
ログインのうえ、現在の設定をダウンロードします。
参考:Commands - Wrangler
npx wrangler login
npx wrangler pages download config zenn-contact-form
そうすると、ルートに wrangler.toml
が作成されます。
# Generated by Wrangler on Fri Nov 08 2024 18:04:42 GMT+0900 (日本標準時)
name = "zenn-contact-form"
pages_build_output_dir = ".svelte-kit/cloudflare"
compatibility_date = "2024-11-05"
KV 名前空間の作成
Cloudflare ダッシュボード(GUI)で作成してもいいですが、ここでは Wrangler(CLI)で作成します。
また、この規模だとプレビュー環境と本番環境は同じでもよい気がしますが、ここではあえて異なる名前空間で管理する方法をまとめます。
攻めの姿勢です。
npx wrangler kv namespace create zenn-contact-form # (リモートの)プレビュー環境用
npx wrangler kv namespace create zenn-contact-form --preview # ローカル環境用
npx wrangler kv namespace create zenn-contact-form-production # プロダクション環境用
こうすると 3 つの id
あるいは preview_id
が生成されます。
それぞれを Wrangler の設定ファイルに反映させます。
キー binding
の値は、SvelteKit から呼び出す際のキーの名称です。
プロジェクト内で一意であればよいので、KV
とします。
# Generated by Wrangler on Fri Nov 08 2024 18:04:42 GMT+0900 (日本標準時)
name = "zenn-contact-form"
pages_build_output_dir = ".svelte-kit/cloudflare"
compatibility_date = "2024-11-05"
+
+ # ローカル環境
+ [[kv_namespaces]]
+ binding = "KV"
+ id = "6653db06adad47b39dd539b60abe5e6c"
+ preview_id = "6653db06adad47b39dd539b60abe5e6c"
+
+ # プレビュー環境
+ [[env.preview.kv_namespaces]]
+ binding = "KV"
+ id = "95ae2b54ae2f4977a23f144e2eb575ef"
+
+ [[env.production.kv_namespaces]]
+ binding = "KV"
+ id = "353de8673ca54e078cfdc52b6735947c"
.gitignore の更新
Wrangler のキャッシュなどを ignore します。
+ # wrangler
+ /.wrangler
SvelteKit の設定
KV
がバインドされていることを明示します。
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
- // interface Platform {}
+ interface Platform {
+ env: {
+ KV: KVNamespace;
+ };
+ }
}
}
export {};
環境構築は以上です!
サーバーサイドの実装
セッションの初期化
パッケージ svelte-kit-sessions
のガイドに従い、hook としてセッションの初期化処理を記述します。
SvelteKit では、リクエストに関連するイベント処理を hooks と呼びます。
ここでは、hooks のうちリクエスト時に呼び出される handle を利用します。
import type { Handle } from '@sveltejs/kit';
import { sveltekitSessionHandle } from 'svelte-kit-sessions';
import KvStore from 'svelte-kit-connect-cloudflare-kv';
import type { Store } from 'svelte-kit-sessions';
declare module 'svelte-kit-sessions' {
interface SessionData {
csrfToken: string;
}
}
export const handle: Handle = async ({ event, resolve }) => {
let sessionHandle: Handle | null = null;
if (event.platform && event.platform.env) {
// https://kit.svelte.dev/docs/adapter-cloudflare#bindings
const store = new KvStore({ client: event.platform.env.KV }) as Store;
sessionHandle = sveltekitSessionHandle({
secret: 'secret',
store
});
}
return sessionHandle ? sessionHandle({ event, resolve }) : resolve(event);
};
CSRF トークンの生成
/contact
へのリクエスト時、生成するようにします。
- export const load: PageServerLoad = async () => {
+ export const load: PageServerLoad = async ({ locals }) => {
+ const { session } = locals;
+ await session.setData({
+ csrfToken: crypto.randomUUID()
+ });
+ await session.save();
+
return {
NAME_MAX_LENGTH,
EMAIL_MAX_LENGTH,
CATEGORY_OPTIONS,
- BODY_MAX_LENGTH
+ BODY_MAX_LENGTH,
+ csrfToken: session.data.csrfToken
+ };
+ };
session.save()
を実行することを忘れないようにしないといけません!(1 敗)
schema の更新
もちろん CSRF トークンはクライアントサイドからのリクエストボディに含まれます。
export const requestBodySchema = z.object({
name: z.string().max(NAME_MAX_LENGTH),
email: z.string().email().max(EMAIL_MAX_LENGTH),
category: z.enum(categoryKeys),
body: z.string().max(BODY_MAX_LENGTH),
+ csrfToken: z.string(),
reCaptchaToken: z.string()
});
CSRF トークンの検証
検証に関するロジックも記述しておきます。
export const actions = {
- default: async ({ request }) => {
+ default: async ({ locals, request }) => {
+ const { session } = locals;
const rawRequestBody = convertToObject(await request.formData());
// バリデーションをかける
const validationResult = requestBodySchema.safeParse(rawRequestBody);
if (!validationResult.success) {
return { success: false, message: 'Invalid request body' };
}
const requestBody = validationResult.data;
+
+ // CSRFトークンを検証
+ const csrfResult = requestBody.csrfToken === session.data.csrfToken;
+ if (!csrfResult) {
+ return { success: false, message: 'Invalid CSRF token' };
+ }
// reCAPTCHAを検証
const captchaResult = verifyCaptcha(requestBody.reCaptchaToken);
if (!captchaResult) {
return { success: false, message: 'Invalid CAPTCHA token' };
}
return { success: true };
}
} satisfies Actions;
クライアントサイドの実装
受け取ったトークンを、hidden な input に入れておくだけです。
<label>
本文
<textarea name="body" maxLength={data.BODY_MAX_LENGTH} disabled={isSubmitting}></textarea>
</label>
+ <input name="csrfToken" type="hidden" value={data.csrfToken} />
<button type="submit" disabled={isSubmitting}>送信</button>
prerender の解除
セッションを利用しているため、prerender はできません。
- export const prerender = true;
- export const prerender = true;
- export const prerender = true;
+ export const prerender = false; // すべてのエンドポイントを一括指定
検証
ローカル環境
Cloudflare KV は Wrangler がローカルでエミュレートしてくれます。
そのため、SvelteKit が Vite を用いて実行している npm run dev
は利用できません。
一度ビルドし、Wrangler に実行してもらう必要があります。
npm run build
npx wrangler pages dev ./.svelte-kit/cloudflare/
きちんと CSRF トークンがクライアントサイドに渡され、input に格納されています!
送信すると、CSRF トークンの検証に成功し、success: true
が返されたため、フォームが初期化されました。
プレビュー環境、プロダクション環境
これをデプロイすると、それぞれの環境でそれぞれの環境の KV に接続し動作します。
一方で、それぞれの環境の中では同一の KV に接続するため、過去のデプロイで作成・編集したデータを引き継ぎます。
Cloudflare ダッシュボードから KV のページに行き、各名前空間の内容を直接見て確認できます。
Wrangler 経由でも確認できます。
おわりに
たったこれだけで Key-Value ストレージ と接続できてしまう!なんと便利な時代なのでしょうか。
次回の Cloudflare D1 も基本的な連携方法は同じです。
ORM も交わる、より深く便利な世界へ進んでいきます!
- ① サイト作成:SvelteKit x Cloudflare Pages
- ② フォーム作成:SvelteKit x Zod x Google reCAPTCHA v3
- ③ セッション管理:SvelteKit x Cloudflare KV ← 次の記事
- ④ データベース管理:SvelteKit x Cloudflare D1
- ⑤ メール送信:SvelteKit x Resend
Discussion