hono × cloudflare workers に sessionを実装した
最近話題のhono × cloudflare workersを使ってIdPでも作ってみようと思ったのですがsessionを管理する機能が無いし、いい感じのOSSもなかったので自前で実装してみました
KVSはcloudflare workers kvを使っています
環境作成
wranglerを入れます
npm install -g wrangler
wranglerが入ったことが確認できたら
wrangler --version
cloudflareにログインします
wrangler login
プロジェクト作成します
wrangler init -y hono-sample
cd hono-sample
hono をインストールします。
npm install hono
src/index.ts をこのように書き換えます
import { Hono } from "hono";
const app = new Hono(); // ①
app.get("/", (c) => {
return c.text("Hello world");
})
export default app;
npm start すると以下のような表示が出ると思います
hono-sample % npm start
> auth-sample-app@0.0.0 start
> wrangler dev
⛅️ wrangler 3.48.0
-------------------
⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:56747
Ready on のところに出てる localhostにアクセスしてみてください
うまくいっていればHello world
が表示されているはずです
kvの作成と設定
workers kvを作成します
wrangler kv:namespace create "SAMPLE_KV"
🌀 Creating namespace with title "worker-SAMPLE_KV"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
{ binding = "SAMPLE_KV", id = "d5098bb808fb4043b4010e531b7e796b" }
wrangler.tomlを編集してください
この部分のコメントアウトをはずして
# [[kv_namespaces]]
# binding = "MY_KV_NAMESPACE"
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
kvを作成したときに表示されたbinding, idを入れます
[[kv_namespaces]]
の部分のコメントアウトを外すのを忘れないようにしてください
[[kv_namespaces]]
binding = "SAMPLE_KV"
id = "d5098bb808fb4043b4010e531b7ebbbb"
こんな感じでkvにアクセスできるようになっているはずです
// src/index.ts
app.get('/', async (c) => {
const test = c.req.query('test') || 'no test'
if (test) {
// @ts-ignore
await c.env.SAMPLE_KV.put('test', JSON.stringify(test));
}
// @ts-ignore
const output_test = await c.env.SAMPLE_KV.get('test') || 'no test'
return c.text(output_test)
})
sessionとsessionMiddlewareの作成
lib/session.tsを作成します
mkdir lib
touch lib/session.ts
session.tsの中身はこんな感じです
sessionIdはmiddlewareで生成して引数で受け取るようにしています
sessionExpireもcookieと有効期限を同じにしたいのでmiddleWareで宣言するようにしています
本当はconstructorで this.data = ロード
みたいにしたかったのですがconstructorでは非同期が使えないみたいなのでdataを使う前にloadDataメソッドを必ず呼び出すようにしています
import { Context } from "hono";
import { KVNamespace } from "@cloudflare/workers-types";
class Session {
private data: Record<string, any> | undefined;
private sessionId: string;
private kv: KVNamespace;
private loaded: boolean = false;
private sessionExpire: number;
constructor(c: Context, sessionId: string, sessionExpire: number = 60 * 60 * 24) {
this.sessionId = sessionId
this.kv = c.env.SAMPLE_KV;
this.sessionExpire = sessionExpire;
}
private async loadData(): Promise<void> {
if (!this.loaded) {
const data = await this.kv.get(this.sessionId)
this.data = data ? JSON.parse(data) : {};
this.loaded = true;
}
}
public async set(key: string, value: any): Promise<void> {
await this.loadData();
this.data![key] = value;
await this.kv.put(this.sessionId, JSON.stringify(this.data), { expirationTtl: this.sessionExpire });
}
public async get(key: string): Promise<any> {
await this.loadData();
return this.data![key];
}
public async delete(key: string): Promise<void> {
await this.loadData();
delete this.data![key];
await this.kv.put(this.sessionId, JSON.stringify(this.data));
}
public async clear(): Promise<void> {
this.data = {};
await this.kv.put(this.sessionId, JSON.stringify(this.data));
}
public async destroy(): Promise<void> {
await this.kv.delete(this.sessionId);
}
}
export default Session;
次にSessionMiddlewareを作成します
mkdir lib
touch middleware/session.ts
中身はこんな感じです
cookieにsessionIdが存在すればそれを取り出し、存在しなければ作成してcookieにsetします
c.sessionにSessionObjをセットしてContextからいつでも扱えるようにします
import { Context } from "hono"
import Session from "../lib/session"
import { getCookie, setCookie } from "hono/cookie"
const SESSION_EXPIRE = 60 * 60 * 24;
function generateAndSetSessionId(c: Context) {
const sessionId = crypto.randomUUID();
setCookie(c, 'session_id', sessionId, { httpOnly: true, path: '/', maxAge: SESSION_EXPIRE});
return sessionId;
}
export const sessionMiddleware = async (c: Context, next: () => Promise<void>) => {
const sessionId = getCookie(c, 'session_id') || generateAndSetSessionId(c);
c.session = new Session(c, sessionId, SESSION_EXPIRE);
await next();
}
src/index.ts でこんな感じでmiddlewareを呼び出して
import { Hono } from 'hono'
import { sessionMiddleware } from '../middleware/session'
const app = new Hono()
app.use('*', sessionMiddleware)
こんな感じでContextからいつでもsessionにアクセスできるようになります!
app.get('/', async (c) => {
const input_test = c.req.query('test')
if (input_test) {
await c.session.set('test',input_test)
}
const output_test = await c.session.get('test') || 'no test'
return c.text(output_test)
})
最後に
ご意見、ご質問等ございましたらお気軽にコメントお願いします!
Discussion