Dev Study
← アプリケーションの土台
レッスン 6 / 11·約 9 分·第7期 アプリケーションの土台

状態を持たないHTTPとCookie/セッション

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

合言葉をどこに置くかで壊れ方が変わります。サーバ側の表か、署名付きでクライアント側か。

まず結論

HTTP は前の要求を覚えません。RFC 9110 は「stateless」を、各要求メッセージの意味が単独で理解できること、と定義します。だから「続き」を表すには、合言葉を毎回クライアントが運ぶしかありません。

その合言葉を、表を引く鍵にするか署名付きで中身ごと預けるか。表を持つ方は実体が増えると割れ、持たない方は発行後に取り消す手順が構造上ありません。

? 読む前に予測

ログイン状態を `const sessions = new Map()` に持つサーバを2台に増やしました。何が起きるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

覚えないと決めた設計。だから合言葉を毎回運ぶ

前のレッスンまでで、サーバがバイト列を受け取り文字列を作って返すところを見ました。そこに「さっきの人だ」という情報はどこにも出てきません。手抜きではなく、規定です。

「各要求の意味が単独で理解できる」「接続との関係が解釈に影響しない」。同じ §3.3 は続けて Many implementations depend on HTTP's stateless design in order to reuse proxied connections or dynamically load balance requests across multiple servers. と述べます。要求を複数のサーバへ動的に振り分けられるのは、この性質に依存した結果です。

さらに §3.3 は禁止事項を課します。接続が保護されその利用者専用である場合を除き、a server MUST NOT assume that two requests on the same connection are from the same user agent 。同じ接続で来たから同じ人だ、という推定は禁じられています。

では「続き」はどう表すのか。毎回の要求に合言葉を入れて運ぶしかありません。定めたのが RFC 6265 です。

HTTP の性質は変えません。ユーザエージェント側に状態を置き、その上に stateful なセッションを載せる構造です。

具体実物

Set-Cookie で合言葉を渡し、Cookie で返してもらう

RFC 6265 §4.1 の定義は短いものです。The Set-Cookie HTTP response header is used to send cookies from the server to the user agent. §4.1.1 によれば、値は name-value ペア1つと続く0個以上の属性値ペアです。

Node で書いた最小のログインエンドポイントの応答を見ます。

$ curl -si http://127.0.0.1:3902/login(Node v24.21.0、macOS、2026-09-16 取得)コマンド出力
HTTP/1.1 200 OK
Set-Cookie: sid=abc; HttpOnly
Content-Type: text/plain; charset=utf-8
Date: Wed, 16 Sep 2026 04:11:16 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 35
 
logged in on 3902; sessions.size=1

2行目が合言葉の受け渡しです。sid=abc が name-value ペア、HttpOnly が属性。本文の sessions.size=1 は、このプロセスの Map に行が1つできたことを示します。HttpOnly は §4.1.2.6 にあります。

スクリプトに cookie を公開する API から省かせる指示です。暗号化ではありません。隣の §4.1.2.5 の Secure も、守られるのは機密性だけで完全性は損なわれうる、と同節が自ら書いています。属性名から期待する効果と規定された効果は別物です。

✓ ここまでの確認

RFC 9110 が HTTP を stateless と定義することから、サーバについて直接導かれるのはどれですか。

抽象原理

合言葉は運べる。だが「表」は運べない

合言葉の運搬はヘッダが引き受けます。では受け取った側は、それをどう「ログイン済みの誰か」に結び付けるのか。方式は2つです。

  1. 鍵にする — 合言葉はただの識別子で、中身はサーバ側の表にある。要求のたびに表を引く。
  2. 中身ごと預ける — 合言葉自体に情報が入り、改竄を検出できるよう署名が付いている。サーバは表を持たない。

これが壊れ方の分かれ道です。1を選ぶと、その表がどこにあるかが可用性の条件になります。表がプロセスのメモリにあるなら、再起動すれば消え、2つになれば表も2つになる。運ばれてきた合言葉は、間違った表には意味を持ちません。

第20週『NAT — アドレスが足りない世界の辻褄合わせ』の NAPT の変換表も同型で、通信が始まった瞬間に1行が作られ使われなくなると消えるソフトステートでした。セッション表も、その行を持つ実体でしか引けません。

具体実物

実体が2つになった瞬間、表が割れる

同じコードを 3902 と 3903 で2つ起動し、3902 でログインして受け取った sid=abc を両方へ送ります。

同じ sid を2つのプロセスへ(Node v24.21.0、macOS、2026-09-16 取得)コマンド出力
$ curl -s -c cookies.txt http://127.0.0.1:3902/login; cat cookies.txt
logged in on 3902; sessions.size=1
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
 
#HttpOnly_127.0.0.1 FALSE / FALSE 0 sid abc
 
$ curl -s -b cookies.txt http://127.0.0.1:3902/whoami
port=3902 sid=abc session={"user":"kazu","port":3902} sessions.size=1
$ curl -s -b cookies.txt http://127.0.0.1:3903/whoami
port=3903 sid=abc session=null sessions.size=0

最後の2行を並べてください。送った cookie は同じ sid=abc です。それでも 3902 は session={"user":"kazu","port":3902} を返し、3903 は session=null、sessions.size=0 を返しました。合言葉はヘッダに乗って届いたが、表は届いていない。 セッションはヘッダではなくプロセスのメモリにある、という事実が出力に現れています。

なお cookies.txt の行が #HttpOnly_ で始まるのは curl が HttpOnly を記録した形式です。curl はブラウザではないのでこの属性の制限を受けません。スクリプトから見えないことは実測していません。

✓ ここまでの確認

「JWT は発行後に取り消せない、と RFC 7519 に書いてある」という説明は妥当ですか。

⇄ 2つを見比べる
具体例 A

サーバ内メモリの Map にセッションを持つ

cookie の値は表を引く鍵で、中身はプロセスのメモリにあります。ログアウトは1行消せば即座に効きます。ただし表はそのプロセスのもので、実体が2つになれば表も2つです。

同じ sid=abc を2つのプロセスへ(Node v24.21.0、macOS)コマンド出力
$ curl -s -b cookies.txt http://127.0.0.1:3902/whoami
port=3902 sid=abc session={"user":"kazu","port":3902} sessions.size=1
$ curl -s -b cookies.txt http://127.0.0.1:3903/whoami
port=3903 sid=abc session=null sessions.size=0
具体例 B

署名付きで中身をクライアントに預ける(JWT 等)

RFC 7519 の JWT は、クレームを JSON で符号化し JWS の payload として署名したものです。サーバは表を持たず、署名の検証だけで受理でき、どの実体でも結果は同じです。

RFC 7519 §7.2 の検証手順に含まれるもの/含まれないもの表
見るもの 発行元への問い合わせ
構文(Compact Serialization) 無い
署名 / MAC の検証 無い
Claims Set の取り出し(JSON として) 無い

表を持つ方式と持たない方式。それぞれ何を失っているでしょうか。

「利用者を今すぐ強制ログアウトさせたい」と言われたら、それぞれの方式で何をするか考えてみてください。

抽象原理

図で見る、合言葉と表の行き先

合言葉はヘッダで運ばれるが、表は実体の中にとどまる
クライアントCookie: sid=abc を毎回添える
要求ごとに振り分け先が決まる
ロードバランサ順に配る方式なら台が入れ替わる
同じ sid=abc が届く
サーバA (3902)表A: sid=abc → kazu
サーバB (3903)表B: 空(size=0)
表を引いた結果
ログイン済みsession={...}
未ログインsession=null
  • ・鍵(sid)は運べる。表は運べない
  • ・署名付きで預ける方式ならAとBの差は消える。代わりに取り消しの手掛かりが消える

この章のまとめ

  • RFC 9110 §3.3 は HTTP を stateless と定義する。§3.4 は HTTP is a stateless request/response protocol と述べる。
  • 同じ §3.3 は、接続が保護されその利用者専用でない限り、同じ接続の2要求が同じ利用者からだと仮定してはならない(MUST NOT) と規定する。
  • だから「続き」は合言葉を毎回運んで表す。RFC 6265 の Abstract は、mostly stateless な HTTP の上に stateful なセッションを維持させる仕組みだと述べる。
  • HttpOnly(§4.1.2.6)はスクリプトに公開する API から cookie を省かせる指示。SameSite は RFC 6265 には無く、RFC 化待ちの 6265bis ドラフトにある。
  • サーバ内メモリの表は実体が増えると割れる。実機では同じ sid=abc に、片方が sessions.size=1、もう片方が session=null を返した。
  • 署名付きで預ける方式は表の到達性から自由だが、RFC 7519 §7.2 の検証手順に発行元への問い合わせが無いため、exp より前の無効化には別の仕組みが要る。
  • 選んでいるのは「状態を持つか否か」ではなく、どちらの壊れ方を引き受けるか。

Cookie がどの要求に添付されるかは、ブラウザ編『分離の単位は origin ではなく site — 妥協が設計に残っている』の site と直結します。既存クイズは ブラウザのアクセス制御 III(Cookieとクレデンシャル) と セキュリティ対策 III(認証とセッション) が対応します。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

RFC 6265 §4.1.2.6 が規定する HttpOnly 属性の効果として正しいのはどれですか。

用語集(5語)
stateless (HTTP)
RFC 9110 §3.3 の定義。各要求の意味が単独で理解でき、接続との関係が解釈に影響しない。§3.4 は `HTTP is a stateless request/response protocol` と述べる。
Set-Cookie
RFC 6265 §4.1 が定める応答ヘッダ。値は name-value ペア1つと 0個以上の属性値ペア。
HttpOnly
RFC 6265 §4.1.2.6 の属性。cookie を HTTP 要求に限定し、スクリプト向けの非 HTTP な API から省くよう指示する。Secure とは独立。
JWT (JSON Web Token)
RFC 7519 が定める、当事者間で移送するクレームの URL-safe な表現形式。クレームは JSON として符号化され、JWS の payload か JWE の平文になる。
ソフトステート
第20週の概念。通信の開始をきっかけに自動で生まれ、使われなくなると消える状態。NAPT の変換表の行。