Open6
CookieとSession
はじめに
RemixでCookieとSessionをどのように扱うか知りたいと思ったので、本スクラップにまとめる。
Cookiesについて
- Cookieはサーバー側で生成される。
- サーバーがHTTPレスポンスで送信する小さな情報でありブラウザは以降のリクエストで送信。
- クッキーを使うことで、認証、カート、ユーザ設定など多くの機能を実装できる。
クッキーの使用
-
前提として、Cokkieはセッションストレージを使用する方が一般的である。
-
loader
やaction
でCookieを操作することが多い -
cookieの作成方法は↓
app/cookies.server.ts
import { createCookie } from "@remix-run/node";
export const userPrefs = createCookie("user-prefs", {
maxAge: 604_800, // one week
});
ルートモジュールの例
app/routes/_index.tsx
import type {
ActionFunctionArgs,
LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
import {
useLoaderData,
Link,
Form,
} from "@remix-run/react";
import { userPrefs } from "~/cookies.server";
export async function loader({
request,
}: LoaderFunctionArgs) {
const cookieHeader = request.headers.get("Cookie");
const cookie =
(await userPrefs.parse(cookieHeader)) || {};
return json({ showBanner: cookie.showBanner });
}
export async function action({
request,
}: ActionFunctionArgs) {
const cookieHeader = request.headers.get("Cookie");
const cookie =
(await userPrefs.parse(cookieHeader)) || {};
const bodyParams = await request.formData();
if (bodyParams.get("bannerVisibility") === "hidden") {
cookie.showBanner = false;
}
return redirect("/", {
headers: {
"Set-Cookie": await userPrefs.serialize(cookie),
},
});
}
export default function Home() {
const { showBanner } = useLoaderData<typeof loader>();
return (
<div>
{showBanner ? (
<div>
<Link to="/sale">Don't miss our sale!</Link>
<Form method="post">
<input
type="hidden"
name="bannerVisibility"
value="hidden"
/>
<button type="submit">Hide</button>
</Form>
</div>
) : null}
<h1>Welcome!</h1>
</div>
);
}
Cookieの属性
- 属性は
createCookie(name, options)
で指定する。 - または
serialize()
でSet-Cookieヘッダ生成時に指定する。 - 下記MDN参照
const cookie = createCookie("cookie-name", {
expires: new Date(Date.now() + 60_000),
httpOnly: true,
maxAge: 60,
path: "/",
sameSite: "lax",
secrets: ["s3cret1"],
secure: true,
});
// デフォルト使用
cookie.serialize(userPrefs);
// 必要に応じて設定の上書きができる
cookie.serialize(userPrefs, { sameSite: "strict" });
Signing cookies
- クッキー受信時に内容を自動検証するには、クッキー署名が必要
- HTTPヘッダの詐称は容易なため、誰かに詐称されたくない場合に効果的。
- Cookieに署名するには、作成時に
secrets
を1つ以上指定する
const cookie = createCookie("user-prefs", {
secrets: ["s3cret1"]
});
- secretsは、配列の先頭に新規シークレットを追加することで、ローテーション可能
- 古いsecretsで署名されたクッキーは、cookie.parse()で正常にデコードされ、cookie.serialize()で作成された送信クッキーへの署名には、常に最新のsecrets(配列の最初のもの)が使われる。
app/cookies.server.ts
export const cookie = createCookie("user-prefs", {
secrets: ["n3wsecr3t", "olds3cret"],
});
app/routes/route.tsx
import { cookie } from "~/cookies.server";
export async function loader({
request,
}: LoaderFunctionArgs) {
const oldCookie = request.headers.get("Cookie");
// oldCookie may have been signed with "olds3cret", but still parses ok
const value = await cookie.parse(oldCookie);
new Response("...", {
headers: {
// Set-Cookie is signed with "n3wsecr3t"
"Set-Cookie": await cookie.serialize(value),
},
});
}
Cookie API
-
createCookie
で返されるcookieコンテナはいくつかのプロパティとメソッドがある。
const cookie = createCookie(name);
cookie.name;
cookie.parse();
// ...etc
ここから紹介する↓
cookie.name
-
Cookie
とSet-Cookie
HTTPヘッダで使われるクッキーの名前。
cookie.parse()
- Cookieヘッダ内のクッキー値を取り出して返すメソッド
const value = await cookie.parse(
request.headers.get("Cookie")
);
cookie.serialize()
- 値をシリアライズし、オプションを組み合わせて
Response
で使用するのに適したSet-Cookie
ヘッダを作成する
new Response("...", {
headers: {
"Set-Cookie": await cookie.serialize({
showBanner: true,
}),
},
});
cookie.isSigned
- クッキーがsecretを使用しているかに応じて、true or falseを返す
let cookie = createCookie("user-prefs");
console.log(cookie.isSigned); // false
cookie = createCookie("user-prefs", {
secrets: ["soopersekrit"],
});
console.log(cookie.isSigned); // true
cookie.expires
- クッキーの有効期限となる日付
- もし
maxAge
とexpires
の両方を持つ場合、maxAge
が優先されるため、この値は現在の日付 + maxAgeとなることに注意
const cookie = createCookie("user-prefs", {
expires: new Date("2021-01-01"),
});
console.log(cookie.expires); // "2020-01-01T00:00:00.000Z"
Sessionについて
- セッションは、サーバー側で同一人物からのリクエストを判別できるようにする仕組み。
- サーバー側のフォーム検証で使われたり、JSがページ上になくても機能する。
- ユーザーを「ログイン」させる多くのサイトにおける基本的な構成要素。
Remixにおけるセッション管理
- セッションは
sessionStorage
オブジェクトを使って、loader
やaction
メソッドで各ルートで管理されるもの。 -
sessionStorage
は、クッキーの解析と生成、DBやファイルシステムへのセッションデータ保存が可能。
Sessionの使い方
下記はcookieSessionStorage
を使った例
app/sessions.ts
import { createCookieSessionStorage } from "@remix-run/node";
type SessionData = {
userId: string;
};
type SessionFlashData = {
error: string;
};
const { getSession, commitSession, destroySession } =
createCookieSessionStorage<SessionData, SessionFlashData>(
{
// a Cookie from `createCookie` or the CookieOptions to create one
cookie: {
name: "__session",
// これらは任意
domain: "remix.run",
httpOnly: true,
maxAge: 60,
path: "/",
sameSite: "lax",
secrets: ["s3cret1"],
secure: true,
},
}
);
export { getSession, commitSession, destroySession };
- ログインフォームはこんなイメージ↓
app/routes/login.tsx
app/routes/login.tsx
import type {
ActionFunctionArgs,
LoaderFunctionArgs,
} from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getSession, commitSession } from "../sessions";
export async function loader({
request,
}: LoaderFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
if (session.has("userId")) {
// すでにログイン済みならばホームへリダイレクト
return redirect("/");
}
const data = { error: session.get("error") };
return json(data, {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
export async function action({
request,
}: ActionFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
const form = await request.formData();
const username = form.get("username");
const password = form.get("password");
const userId = await validateCredentials(
username,
password
);
if (userId == null) {
session.flash("error", "Invalid username/password");
// Redirect back to the login page with errors.
return redirect("/login", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
session.set("userId", userId);
// ログイン成功後、ホームへリダイレクトさせる
return redirect("/", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
export default function Login() {
const { error } = useLoaderData<typeof loader>();
return (
<div>
{error ? <div className="error">{error}</div> : null}
<form method="POST">
<div>
<p>Please sign in</p>
</div>
<label>
Username: <input type="text" name="username" />
</label>
<label>
Password:{" "}
<input type="password" name="password" />
</label>
</form>
</div>
);
}
- ログアウトフォームはこんなイメージ↓
logout.tsx
import { getSession, destroySession } from "../sessions";
export const action = async ({
request,
}: ActionFunctionArgs) => {
const session = await getSession(
request.headers.get("Cookie")
);
return redirect("/login", {
headers: {
"Set-Cookie": await destroySession(session),
},
});
};
export default function LogoutRoute() {
return (
<>
<p>Are you sure you want to log out?</p>
<Form method="post">
<button>Logout</button>
</Form>
<Link to="/">Never mind</Link>
</>
);
}
CreateSessionStorage
- 独自データベースにセッションを保存する方法の一つ?
- Cookieとセッションデータ管理用のCRUDメソッドのセットを必要とする。
-
createData
- クッキーにセッションIDが存在しない場合、初期セッション生成時に
commitSession
から呼ばれる
- クッキーにセッションIDが存在しない場合、初期セッション生成時に
-
readData
- クッキーにセッションIDが存在する場合、
getSession
から呼ばれる
- クッキーにセッションIDが存在する場合、
-
updateData
- クッキーにセッションIDが存在する場合、
commitSession
から呼ばれる
- クッキーにセッションIDが存在する場合、
-
deleteData
-
destorySession
から呼ばれる
-
-
- CookieはセッションIDの永続化に使用している。
使い方
import { createSessionStorage } from "@remix-run/node"; // or cloudflare/deno
// DBにセッションストレージを作成する関数
function createDatabaseSessionStorage({
cookie,
host,
port,
}) {
// 好みのDBクライアント設定
const db = createDatabaseClient(host, port);
return createSessionStorage({
cookie,
async createData(data, expires) {
const id = await db.insert(data);
return id;
},
async readData(id) {
return (await db.select(id)) || null;
},
async updateData(id, data, expires) {
await db.update(id, data);
},
async deleteData(id) {
await db.delete(id);
},
});
}
- 呼び出すときはこう↓
const { getSession, commitSession, destroySession } =
createDatabaseSessionStorage({
host: "localhost",
port: 1234,
cookie: {
name: "__session",
sameSite: "lax",
},
});
CreateCookieSessionStorage
- cookieベースのセッション(cookieをセッションストレージとして扱う手法)
- メリットは追加のBEサービスやDBを必要としない点。
- デメリットは、ブラウザの最大許容cookieのサイズ(4kb)を超過できない点。
- 欠点は、ほとんどすべてのローダとアクションで commitSession を行わなければならないことです。ローダーやアクションがセッションを少しでも変更したら、それをコミットしなければなりません。つまり、あるアクションでsession.flashを実行し、別のアクションでsession.getを実行した場合、flashされたメッセージを消すためにはコミットしなければなりません。他のセッション保存戦略では、セッションが作成されたときだけコミットする必要があります(ブラウザのクッキーはセッションデータを保存しないので、変更する必要はありません。)
- 他のセッションの実装はクッキーに一意なセッション ID を保存し、その ID を使って真実の情報源(インメモリ、ファイルシステム、 DB など)でセッションを検索することに注意してください。クッキー・セッションでは、クッキーが真実の情報源なので、一意な ID は最初からありません。クッキー・セッションで一意な ID を追跡する必要がある場合、session.set() によって ID 値を自分で追加する必要があります。
〜参考〜
使い方
import { createCookieSessionStorage } from "@remix-run/node"; // or cloudflare/deno
const { getSession, commitSession, destroySession } =
createCookieSessionStorage({
cookie: {
name: "__session",
secrets: ["r3m1xr0ck5"],
sameSite: "lax",
},
});
Session API
-
getSession
で取得したセッションオブジェクトは、いくつかのメソッドとプロパティを持っている。
session.has(key)
- セッションに指定した変数がある場合、trueを返却する
session.has("userId");
session.set(key, value)
- 以降のリクエストで使用するセッション値を設定する
session.set("userId", "1234");
session.flash(key, value)
- 初回読み取り時に、unset()されるセッション値を設定して、あとで消える
- フラッシュメッセージやサーバーサイドのフォーム検証メッセージに便利
import { commitSession, getSession } from "../sessions";
export async function action({
params,
request,
}: ActionFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
const deletedProject = await archiveProject(
params.projectId
);
// ✅ここでメッセージ出す
session.flash(
"globalMessage",
`Project ${deletedProject.name} successfully archived`
);
return redirect("/dashboard", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
session.get()
- 前回リクエストのセッション値の取得
session.get("name");
session.unset()
- セッションから値を取り除く
session.unset("name");
- また
cookieSessionStorage
を使用する場合、unset
時にはsessionをコミットしなければならないことに注意!
export async function loader({
request,
}: LoaderFunctionArgs) {
// ...
return json(data, {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}