はじめに
開発初心者が頑張ってサイトを作っていたら、管理者だけが使える API を作りたくなりました。
少し前の記事でその鱗片が少し見えた気もしますが、それはさておき、実装に丸一日かかってしまったので、備忘録も兼ねてまとめていこうと思います。
経緯なども書いていくため、結論( Cookie を使った実装方法)だけ知りたい方はこちらから飛んでください。
この記事を書く原因となりやがったコミットは以下に置いておきます。
「より良い実装があるよ!」「ここは意味が違うよ!」などの気づいた点があれば、気軽にコメントや issue を投げていただけると嬉しいです。
開発初心者の記事なので、基礎の基礎からメモしています。ご了承ください...
問題のコミット:
Cookie を使うと何が嬉しいの?
イメージとしては、まず最初は以下のような流れになります。
ここでセッションIDを Cookie として渡しておくことで、次回以降のリクエストは次のような流れになります。
管理者はわざわざパスワードを何回も打つ必要がなくなるのです!
以降の管理者向け API では、この Cookie に含まれるセッションIDを使ってログイン済みかどうかを判定します。
ちょこっと雑談
201: リソースが新しく作られた時のステータスコード
401: 認証失敗のステータスコード
覚えておくと便利かも...
なお、 Cookie は Google Chrome なら『検証ツール -> Application -> Cookies』で見ることができます。

ちょっとまった。セッションIDってなに?
ここまで当然のようにセッションIDという言葉を使ってきましたが、そもそもこれはなんなのでしょうか?
一言でまとめると、『サーバが誰なのかを認識するための目印』がセッションIDになります。
では、セッションIDが公開鍵暗号みたいに何かしらの暗号なのかというと違います。
セッションIDは、全く意味のないただのランダムな文字列です。
何かを暗号化しているわけではありません。
では、どうやって識別するのでしょうか?
答えは、『サーバ側で対応表を持っておく』です。
そして、各それぞれのセッションIDには基本的に有効期限があります。
ずっと同じセッションIDを使い続けるのは危険だからです。
有効期限が切れたセッションIDは無効とし、再度ログインを要求することで、安全性を保っています。
実装をする
では、私の世界で最も最悪なコードを使って説明していきます。
Cookie 製造&配達部分
まずは、セッションを保持するための「箱」を用意します。
本来であれば Redis 等を使うべきなのでしょうが、私が初心者なので単純なオブジェクトで管理しました。
// session id of dictionary
let sessions = {};
セッションIDはランダム性のために crypto.getRandomValues() や crypto.randomUUID() を使った方がいいらしいです。
正直なところ、当時(今)はまだよくわかっておらず、有効期限も一時間だし問題ないという楽観的考えのせいで Math.random() を使用しています。
そして、有効期限を決めます。
createdAt はデバッグ用に追加しているだけなので、最低限必要なのはセッションIDと有効期限だけです。
function createSession(sessionId, name) {
const createdAt = Date.now();
// SESSION_TTLは3600が代入される環境変数です。無視してください。
const expiresAt = Date.now() + SESSION_TTL * 1000;
return {
sessionId: sessionId,
createdAt: createdAt,
expiresAt: expiresAt,
site: name, // これは無視してください。関係ないです。
};
}
...
// セッションの場所
const session = createSession(sessionId, site_config.name);
sessions[sessionId] = session // セッションの箱に突っ込む
次に、今回の記事の山場である Cookie を触っていきます。
const cookie = {
"Set-Cookie": "sessionId=" + sessionId + ";HttpOnly;Max-Age=3600",
};
この Set-Cookie ヘッダーをレスポンスに含めることで、ブラウザ側にセッションIDが保存され、次回以降のリクエストで自動的に送信されます。
では、分解していきましょう。
sessionId= ~~~ ; ではサーバが発行したセッションIDを示します。
なお、ここの部分は自由に指定ができ、 Cookie に保存したいものを <name>=<value>; で書けばいいです。
Cookie には機密情報そのものは入れず識別用のIDを保存するのが基本です。
間違ってもパスワードなどの認証情報を直接入れないようにしてください。
次に、 HttpOnly です。
これがあることによって、 JavaScript からアクセスできなくなります。
セッションIDを JavaScript で盗もうとすることを防ぐことができます。
最後に、 Max-Age です。
これは、この Cookie の有効期限を示します。単位は秒です。
今回は 3600 秒、つまり 1 時間です。
今回は使わなかった属性も解説します。
Expires=~~~;
これは Max-Age の日付版です。単位は日です。
Secure;
これは HTTPS を使用時にのみ Cookie を送信するようにするものです。
常時SSLのサイトでは基本的にこれを使うべきですね。
SameSite=~~~;
これは、他サイトからのリクエスト時に Cookie を送るかどうかを制御する属性です。
値は代表的な三つを提示しておきます。
- SameSite=Strict;
- 他のサイトからは一切送られない
- SameSite=Lax;
- 通常の遷移では送られる
- SameSite=None;
- 常に送られる
Cookie 受け取り部分
次に、サーバの受け取り部分を作ります。
先ほど作った Cookie を取得して検証する部分です。
今回は以下のような関数を作って取得しました。
Cookie は req.headers.cookie を使うと sessionId=bg3b96130... のような文字列を取得することができます。
そのため、 .split("=")[1] のように = で分離してセッションIDを取得しなければなりません。
function getSession(req) {
if (req.headers.cookie === undefined) return;
const sessionCookie = req.headers.cookie
.split("; ")
.find((c) => c.startsWith("sessionId="));
if (sessionCookie === undefined) return;
const sessionId = sessionCookie.split("=")[1];
const session = sessions[sessionId];
if (session === undefined) return;
// 有効期限チェック
if (session.expiresAt <= Date.now()) {
delete sessions[sessionId]; // 有効期限が切れていたら捨てる
return;
}
return session;
}
コード内では "; " で分けてから "sessionId=" で始まるものを探しています。
const sessionCookie = req.headers.cookie
.split("; ")
.find((c) => c.startsWith("sessionId="));
これは、Cookie には複数のキーが含まれる可能性があるためです。
例えば、sessionId=ABC;hoge=hoge のようにセッションID以外も返ってきたとします。
この場合文字列なので、 = で分離すると ABC;hoge=hoge が取得されてしまいうまくいきません。
本来は起きにくいですが、ローカル環境で実行する前提なので、他作品のクッキーが localhost:5500 に残っている可能性があります。
そのためこのような処理を入れているのです。
そして、取得したセッションIDを検証します。
ここは実装に左右される部分が大きいので詳細の説明は省きます。
単純に getSession(req); で取得し、検証するだけです。
// ログインチェック
const session = getSession(req);
if (session === undefined || session.site !== path[1]) {
// ダメなら login 画面にリダイレクト
res.writeHead(303, { Location: `/${path[1]}/login` });
res.end();
} else {
// 有効ならコントロールパネルを表示
response(res, 200, "html", ADMIN_CONTOL_PANEL_HTML_PATH);
}
ちょこっと雑談
301: 永久的なURL変更を示すリダイレクトのステータスコード
302: 一時的なURL変更を示すリダイレクトのステータスコード
303: 他のURLを参照のステータスコード
以下のコードでリダイレクトすることができる。
res.writeHead(303, { Location: "<path>" });
res.end();
おわりに
今回は、管理者専用 API を作る過程で学んだ Cookie を使った簡易的な認証処理についてまとめました。
正直、実装はかなり終わっていますが、「なぜこうなっているのか」を知る良い経験になったと思います。
同じようなものを作ろうとしている人の参考になれば幸いです。
最後まで読んでいただき、ありがとうございました。