Remixとexpress-sessionでセッションを共有する
公開日:
はじめに
現在自分は仕事でNext.js
で運営しているサイトをRemix
でリファクタする施策に参加しています。リバースプロキシをたて、同一ドメイン上で古い Next.js
サーバーと Remix
サーバーにリクエストを振り分けている構成になっています。その施策の中でNext.js
サーバーとRemix
サーバーでセッションを共有する必要があったので備忘録として方法を書き残します。
Remix
に置き換えるまでの応急処置で、置き換わったら削除されるであろう処理なのでだいぶゴリ押しで実装しています。
やりたいこと
/login
にリクエストを送ると既存のNext.js
サーバーからレスポンスが返ってくる/hoge
にリクエストを送るとRemix
サーバーからレスポンスが返ってくる
この時、
- ユーザーが
Next.js
サーバーでホスティングされているログインページでログイン Next.js
でホスティングされている別のページを見にいく ← cookie を用いてセッションが保持されている(ユーザーの認証情報が保持されている)Remix
でホスティングされている/hoge
を見にいく ← ここでもセッションが保持されていて欲しい
これがこの記事で達成したいことの概要です。
この記事ではexpress-session
でセットされた cookie を Remix
で読み取る処理を実装します。
前提
- Next.js 側のセッション管理は
express-session
というライブラリを使用(参考) - Remix 側では、Remix が用意している
sessionAPI
を使用(参考) - リバースプロキシを通して 2 つのサーバーを立てているのでドメインは同一のものを使用
- この記事では以下のバージョンを使用
express-session
: "1.17.3"@remix-run/node
: "1.16.1"@remix-run/react
: "1.16.1"@remix-run/serve
: "1.16.1"
概要
ドメインは同一なのでどちらのサーバーにも cookie の情報は送られてきます。ただ、express-session
と Remix
のsession API
では cookie に値を入れるときの処理が異なるため、そのままではお互いがセットした cookie の中身を解読することはできません。なので、間にお互いの処理に互換性を持たせる変換処理を挟んであげます。
上の画像のようなイメージです。
session を cookie で保持する多くの場合、session そのものを cookie で保持するのではなく、ユーザーの情報などが含まれる session の本体は DB などに保存しておき、その session の id などを cookie に保存する場合が多いでしょう。ただ、そうする場合はセキュリティ上の理由で生の id を文字列として保持しておくことは良くなく、基本は暗号化されてブラウザ側に cookie として保存しています。
その暗号化は
- session の id を暗号化して送信
- 送信されてきた暗号を解読して session の id を取り出す
これさえできればいいのでライブラリによって暗号化の処理はまちまちです。
今回の例では、express-sesion
でセットした cookie を Remix
で解読できればいいので、ブラウザから送られてきた cookie に対して以下の処理を行います。
- 送られてきた cookie(
express-session
による暗号化済)に対して、express-session
のソースからコピペしてきた処理で解読し、生の id を取り出す - Remix のソースからコピペしてきた方法で暗号化する
これらの処理を行った cookie を Remix
のsession API
に渡すことで Remix
が cookie を解釈することができます。
一旦それぞれの暗号化処理の詳細を見ていきましょう
express-session の処理
https://github.dev/expressjs/session
暗号化
- secret の文字列を用いて
cookie-signature
というライブラリで暗号化 - 暗号化された文字列の先頭に
s:
という文字列を追加(おそらく、『暗号化されてるよ』っていう意味) connect.sid
という名前で cookie にセット
解読
- リクエストヘッダから
connect.sid
という名前の cookie 取り出す - 先頭の
s:
の文字を削る - secret 文字列を用いて
cookie-signature
で解読
Remix の session API の暗号化処理
https://github.com/remix-run/remix/blob/main/packages/remix-server-runtime/cookies.ts
暗号化
- Remix 独自の encode 処理をする(参照)
- secret 文字列を用いて
cookie-signature
で暗号化 - 指定された名前で cookie にセット
解読
- リクエストヘッダから指定された名前の cookie 取り出し、
cookie.parse
で cookie 文字列からオブジェクトに変換 - secret 文字列を用いて
cookie-signature
で解読 - オリジナルの decode 処理をする(参照)
最終的な実装
以下のコードは Remix が提供する loader 関数 です https://remix.run/docs/en/main/route/loader
cookie にユーザー情報があればユーザーを取得してコンポーネントに渡しています。
export async function loader({ request }: LoaderArgs) {
// リクエストのヘッダからcookieを取り出す
const cookie = request.headers.get('Cookie');
// express-session => Remixの変換処理
const compatibleCookie = getCompatibleCookies(cookie);
// session idからユーザーを取得
const user = await getSessionUser(compatibleCookie);
return json({
user,
});
}
最も重要な変換処理のコードは以下です。
import { parse } from 'cookie';
import { unsign, sign } from 'cookie-signature';
// express-session側でデフォルトになっているcookieの名前
const COOKIE_NAME = 'connect.sid';
// 暗号化に必要なシークレット文字列
const COOKIE_SECRET = 'hoge';
/**
* copied from https://github.com/remix-run/remix/blob/main/packages/remix-server-runtime/cookies.ts#L223
* Remixのソースからコピーしてきた処理
*/
function myUnescape(value: string): string {
const str = value.toString();
let result = '';
let index = 0;
let chr;
let part;
while (index < str.length) {
chr = str.charAt(index++);
if (chr === '%') {
if (str.charAt(index) === 'u') {
part = str.slice(index + 1, index + 5);
if (/^[\da-f]{4}$/i.exec(part)) {
result += String.fromCharCode(parseInt(part, 16));
index += 5;
continue;
}
} else {
part = str.slice(index, index + 2);
if (/^[\da-f]{2}$/i.exec(part)) {
result += String.fromCharCode(parseInt(part, 16));
index += 2;
continue;
}
}
}
result += chr;
}
return result;
}
/**
* copied from https://github.com/remix-run/remix/blob/main/packages/remix-server-runtime/cookies.ts#L182
* Remixからコピーしてきた処理
*/
function encodeData(value: string | boolean): string {
return btoa(myUnescape(encodeURIComponent(JSON.stringify(value))));
}
/**
* 変換処理の本体
* express-sessionの処理でcookieを解読して生の値を取り出した上でRemixの処理で暗号化する関数
*/
export const getCompatibleCookies = (
cookieStr: string | null,
): string | null => {
if (cookieStr == null) return null;
// cookie文字列から対象となる値を取得
const cookieObj = parse(cookieStr);
const sessionIdFromCookie = cookieObj[COOKIE_NAME];
if (sessionIdFromCookie == null) return null;
// 取り出した文字列から's:'の文字を取り除いて、解読
const rawValue = unsign(sessionIdFromCookie.slice(2), COOKIE_SECRET);
// Remixの処理で暗号化
const encodedData = encodeData(rawValue);
const signedValue = sign(encodedData, COOKIE_SECRET);
// https://regex101.com/r/SXYDhc/1
const regExp = new RegExp(`(?<=${COOKIE_NAME}=)(.*?)(?:;|$)`);
const replacedCookieString = cookieStr.replace(regExp, signedValue);
return replacedCookieString;
};
これが先ほど詳細を書いた変換処理です。この関数を挟むことによってexpress-session
と Remix
のsession API
間でセッションの共有ができるようになります。
よければ参考にしてみてください。
最後に
割と大変な要件で、ゴリ押しの実装ではありましたが、完全に Remix に置き換わるまでの応急処置だということを考えれば許せるかなと思います。ライブラリの中身を読んでそのコードを元に色々手を加えるというあるハッカーのような体験ができて面白かったです。
では
Bye