HTTP はステートレスなのに、なぜブラウザはログイン状態を覚えていられるのか?
EC サイトでログインしたまま何ページ遷移しても、いちいちログアウトさせられることはない。だが HTTP は「1 つ前のリクエストで誰が来たか」を一切覚えていないステートレスなプロトコルだと聞いたことがあるはずだ。矛盾しているように見えるこの 2 つの事実の間を埋めているのは、たった 1 行のレスポンスヘッダーである。
- HTTP 自体は各リクエストを独立に処理するステートレスなプロトコルで、サーバーは前回のリクエストを覚えていない。ログイン状態を保つのは
Set-Cookie/Cookieヘッダーというアプリケーション層の仕組みだ。 - サーバーが
Set-Cookieで発行した値を、ブラウザは Domain / Path が一致するリクエストにだけ自動で付け直す。Expires/Max-Ageを省略すると「セッション Cookie」としてブラウザを閉じると消える。 HttpOnly/Secure/SameSiteという 3 つの属性は既定で付いておらず、明示しない限り Cookie は JavaScript から読めて平文回線でも送信される。
「ステートレス」とは何を捨てた設計なのか
ブラウザがサーバーにリクエストを送るたびに、サーバーはそのリクエスト単体だけを見て応答を返す。ステートレスとは、直前のリクエストで誰が何を送ってきたかをサーバー自身が記憶していない性質を指す。1 回のリクエストに含まれる情報だけで処理が完結するため、どのサーバーが応答しても結果が変わらず、ロードバランサーで複数台に処理を分散しやすくなる。
裏を返すと、「さっきログインした人物と、今このリクエストを送っている人物が同一である」という事実を、HTTP プロトコル自身は一切保証しない。この情報を毎回のリクエストに載せて運ぶ役目を担うのが Cookie だ。
Set-Cookie / Cookie ヘッダーが運ぶ往復
サーバーはレスポンスにSet-Cookieヘッダーを付け、任意の名前=値のペアをブラウザに渡せる。ブラウザはこれを保存し、以後Set-Cookieで指定された Domain / Path の条件に一致するリクエストへ、Cookieリクエストヘッダーとして自動で付け直す。この「サーバーが渡し、ブラウザが自動で返す」という一方向の橋渡しがなければ、サーバーは毎回のリクエストがどのユーザーのものかを知る手立てを持たない。
┌── Cookie のラウンドトリップ ──────────┐ │ ① レスポンス(サーバー→ブラウザ) │ │ Set-Cookie: session_id=abc123 │ │ │ │ ② ブラウザが保存 │ │ Domain/Path が一致する間だけ保持 │ │ │ │ ③ 以降のリクエスト(毎回自動で付与) │ │ Cookie: session_id=abc123 │ └───────────────────────────────────────┘
Cookie には有効期限を指定できる。Expires / Max-Age の両方を省略すると「セッション Cookie」として扱われ、ブラウザを閉じると破棄される(両方指定した場合は Max-Age が優先される)。「ブラウザを再起動したらログイン状態が消えていた」という体験の裏には、たいていこの指定漏れがある。
Cookie は会員クラブの入館証に近い。受付で Set-Cookie(入館証の発行)を受け取った客は、以降ゲートを通るたびに入館証を提示するだけで、毎回身分証明をやり直さずに済む。ただし、その入館証に HttpOnly や Secure に当たる偽造防止の細工が無ければ、ロビーに落ちていた入館証を誰かが拾うだけで、同じ客として振る舞えてしまう。
何も指定しないと危険な 3 つの属性
Set-CookieにはHttpOnly・Secure・SameSiteという 3 つの属性を追加できるが、いずれも既定では付いていない。HttpOnlyを付けなければ、その Cookie は JavaScript の document.cookie から読み取れる状態のままで、XSS でスクリプトを実行されるとログイン Cookie がそのまま盗まれる。Secureを付けなければ、暗号化されていない HTTP 接続でも Cookie が送信され、通信経路上で盗聴される余地が残る。
SameSite属性は、その Cookie を他サイトを起点にしたリクエストにも送るかを制御する。Strictは自サイト発のリクエストにしか付けず、Laxは自サイト発に加えてリンククリックのような安全なナビゲーションにも付ける。SameSiteを省略した場合、現在の主要ブラウザはLaxとして扱う。ただしLaxでも一部のクロスサイトリクエストには送られてしまうため、CSRF 対策としてはこれだけに頼らずトークン検証を併用する必要がある。
Cookie の中身は「印」なのか「データそのもの」なのか
多くの実装では、Cookie にはセッションを識別するためのランダムな文字列(セッション ID)だけが入っていて、ログインユーザーの実データはサーバー側のデータストアに保存されている、と考えられがちだ。しかし実際には、Cookie 自体にデータそのものを詰め込む実装も広く使われている。Rails のデフォルトのセッションストアである ActionDispatch::Session::CookieStore は、secret_key_base で暗号化した値としてセッションハッシュそのものを Cookie に格納し、サーバー側の追加ストアを必要としない。
この方式には明確な上限がある。Cookie の一般的なサイズ制限はおよそ 4096 バイトで、Rails はこれを超えて session に値を詰めようとすると CookieOverflow 例外を送出する。ユーザー ID やフラッシュメッセージ程度なら問題なく収まるが、複雑なオブジェクトをセッションへ丸ごと入れる実装は、この上限に不意に突き当たることになる。
ブラウザの DevTools(Chrome の Application タブ、Firefox のストレージタブ)で Cookie の一覧を開くと、Domain・Path・Expires・HttpOnly・Secure・SameSite の列がそのまま並び、今日解説した属性を 1 つずつ確認できる。Rails の session[:user_id] = user.id のような一行は、実際にはこの CookieStore の仕組みに乗ってブラウザへ書き戻されており、セッションに大きなオブジェクトを詰めた際は CookieOverflow で初めてこの仕組みに気づくことになる。
リクエスト検証用に公開されている httpbin へ curl を送り、Set-Cookie / Cookie の往復を手動で再現する。
# ① サーバーが Cookie を発行する場面(Set-Cookie を確認) curl -s -i "https://httpbin.org/cookies/set?session_id=abc123" | grep -i "^set-cookie\|^HTTP" # ② 受け取った Cookie を次のリクエストで送り返す場面 curl -s -i -b "session_id=abc123" "https://httpbin.org/cookies"
①ではレスポンスヘッダーに set-cookie: session_id=abc123; Path=/ が含まれ、302 で /cookies へリダイレクトされる。②では -b で指定した値が Cookie リクエストヘッダーとして送られ、レスポンスの JSON に {"cookies": {"session_id": "abc123"}} としてサーバー側で受信できたことが確認できる。ブラウザが普段自動でやっている往復を、手動で再現した形だ。
- Cookie を削除すればログアウトしたことになる — ブラウザ側の保存をやめただけで、サーバー側でセッションやトークンを無効化していなければ、盗まれた Cookie の値は有効期限内なら別のブラウザからでもそのまま使える。
- HttpOnly を付ければ Cookie の通信は安全になる — HttpOnly は JavaScript からの読み取りを防ぐだけで、通信経路が暗号化されているかどうかは Secure 属性が担う別の役割だ。
- Cookie には常にセッション ID だけが入っている — Rails の CookieStore のように暗号化したデータそのものを Cookie に格納する実装もあり、中身が何を意味するかは実装依存だ。
- ステートレス
- サーバーが直前のリクエストの情報を覚えておらず、各リクエストを独立に処理する性質。
- Set-Cookie ヘッダー
- サーバーがレスポンスに付与し、ブラウザに名前=値のペアを保存させる HTTP ヘッダー。
- セッション Cookie
- Expires / Max-Age を指定しない Cookie の呼び方。ブラウザを閉じると破棄される。
- HttpOnly
- その Cookie を JavaScript の document.cookie から読み取れなくする属性。
- Secure
- 暗号化された HTTPS 接続でのみその Cookie を送信させる属性。
- SameSite
- 他サイトを起点にしたリクエストにその Cookie を送るかどうかを制御する属性(Strict / Lax / None)。
- Using HTTP cookies - MDN — Cookie の属性・ライフサイクルの全体像を一次情報でおさえられる公式ガイド。
- Set-Cookie - HTTP - MDN — 各属性の構文と既定の挙動を仕様に近い形で確認できるリファレンス。
- ActionDispatch::Session::CookieStore - Rails API — Rails のデフォルトセッションストアが Cookie に何をどう格納しているかを説明する一次資料。