ログインしたあと、別のページへ移ってもログインは続きます。HTTP は、前のリクエストを覚えていません。続いているように見えるのは、ブラウザがセッションIDを、次のリクエストへ自分で付けているからです。
サーバーは、名前と値をブラウザに預けます。ブラウザは、条件を満たすリクエストにだけ、その名前と値を戻します。
用語
Cookie
ブラウザが保存する、名前と値の組です。次のリクエストでは Cookie ヘッダに、名前と値だけが戻ります。
Set-Cookie
サーバーがレスポンスに載せるヘッダです。ブラウザに、名前と値を保存させます。
Set-Cookie: session_id=s-1; Path=/; SameSite=Lax
この例で保存されるのは session_id=s-1 です。次のリクエストで戻るのは Cookie: session_id=s-1 だけで、Path や SameSite は戻りません。この2つは、ブラウザがどのリクエストにその値を付けるかを決めるルールです。
セッションID
ログイン後にサーバーが発行する識別子です。ユーザー情報そのものはサーバー側にあり、付いてきたIDから引きます。
Same-site
Cookie のサイトと、リクエストを始めたサイトが一致していることです。app.example.com と api.example.com は、ホストが違っても同じサイトです。example.com とそのサブドメインで、スキームも同じだからです。https://example.com と http://example.com は別サイトです。
Cross-site
別のサイトを起点に、対象のサイトへリクエストが飛ぶことです。evil.example から bank.example への送信がこれにあたります。
HTTPは前の通信を覚えない
HTTP は、リクエストのたびに独立しています。サーバーは、プロトコルだけでは「さっきの通信の続き」だと分かりません。
Cookie は、その続きをブラウザに持たせるために作られました。ブラウザが次のリクエストへ同じ名前と値を自分で付けるので、サーバーは「さっきと同じブラウザからだ」と判断できます。
1994年、Netscape の Lou Montulli が、買い物かごのようにページをまたいで状態を保つ必要から入れました。ログインは、そのあと特によく使われるようになった場面です。認証が通ったあと、サーバーはセッションIDを Cookie に預けます。次のリクエストでそのIDを受け取り、サーバー側に置いたユーザー状態を引きます。
sequenceDiagram participant B as ブラウザ participant S as サーバー S->>B: Set-Cookie session_id=s-1 Note over B: 名前と値を保存する B->>S: Cookie session_id=s-1 Note over S: IDからユーザー状態を引く
図: サーバーがセッションIDを預け、ブラウザが次のリクエストに名前と値だけを戻す。
【ログイン直後】サーバー → ブラウザ
サーバー:「セッションIDは
s-1。次のリクエストに付けてください」
【次のページ】ブラウザ → サーバー
ブラウザ:「
/accountへ行きます。預かったsession_id=s-1を付けました」
サーバー:「
s-1はログイン済みのセッションです。このユーザーの状態を返します」
付けるのはブラウザです。ページ上のスクリプトは、このヘッダを組み立てません。
主な用途
預ける中身は、用途で違います。
| 用途 | 預けるもの | 状態の置き場所 |
|---|---|---|
| ログイン状態の維持 | セッションID | サーバー側。IDから引く |
| ユーザー設定 | 言語やテーマなど、値そのもの | Cookie の中 |
| トラッキング | 流入元のID | Cookie の中。成約時にサーバーが読む |
ユーザー設定は、言語や表示テーマのように、次のリクエストでサーバーが知る値です。
トラッキングでは、紹介リンクに付いた流入元IDを、広告主側が Set-Cookie で預かります。あとで成約のリクエストにブラウザがそれを付けるので、どの紹介元の成果かが分かります。
バックエンドからもフロントからも付けられる
保存したあとの自動送信は、どちらが付けたかで変わりません。
バックエンドは、レスポンスの Set-Cookie で預けます。フロントは、document.cookie への代入で預けます。どちらもブラウザの保存先は同じです。次以降、条件に合うリクエストへ、ブラウザが Cookie ヘッダで付けます。
document.cookie = "theme=dark; Path=/; Max-Age=2592000; SameSite=Lax";
ログイン用だけは、付与をバックエンドに寄せます。HttpOnly は Set-Cookie でしか付けられません。これを付けると、フロントの JavaScript からは読めません。
ブラウザが付ける条件
ブラウザへ残す指示は6つです。リクエストに付けるかは、Domain、Path、期限、Secure、SameSite が見ます。HttpOnly は、付けたまま JavaScript から読めるかを決めます。
| 属性 | ブラウザが行う判断 |
|---|---|
Domain | どのホストへ付けるか。未指定なら、セットしたホストだけ |
Path | どのパス配下のリクエストに付けるか。未指定なら、セットしたURLのディレクトリ |
Expires / Max-Age | いつまで保存し、期限内のリクエストにだけ付けるか |
Secure | HTTPS のリクエストにだけ付けるか |
SameSite | 別サイトからのリクエストに付けるか |
HttpOnly | リクエストへは付けたまま。JavaScript から読めるかを決める |
Expires と Max-Age の両方があるときは、Max-Age が優先されます。両方なければ、ブラウザを閉じるまでのセッション Cookie です。閉じたあとも、ブラウザの復元で残ることがあります。
SameSite を付けないとき、いまのブラウザは Lax として扱います。
ログインでは HttpOnly で預ける
ログインでは、サーバーがセッションIDを Set-Cookie し、HttpOnly を付けます。
Set-Cookie: session_id=s-1; Path=/; HttpOnly; Secure; SameSite=Lax
HttpOnly が止めるのは、JavaScript が document.cookie からその値を読むことです。XSS で注入されたスクリプトは、セッションIDを抜き取って外へ送る、という読み取りができません。
ログイン状態は、サーバーがセットしたIDを、ブラウザが戻すだけで足ります。ページ上のスクリプトが、そのIDを読む場面はありません。
別サイトからの送信は SameSite で決まる
SameSite は、別サイトを起点にしたリクエストに、その Cookie を付けるかを決めます。
送信先のホストが条件に合うと、ブラウザは Cookie を自動で付けます。SameSite が別サイトからの送信を許していると、認証 Cookie が付き、サーバーには本人の操作に見えます。これが CSRF です。
【別サイトからの POST】evil.example → bank.example
evil.example:「銀行の振込URLへ、POST を飛ばしたよ」
ブラウザ:「
SameSite=Laxだから、別サイトからの POST にはsession_idを付けない」
bank.example:「Cookie が無い。この振込は受けない」
SameSite=None かつ Secure のときは、この POST にもセッションIDが載ります。銀行側には、ログイン済みユーザー本人からの振込に見えます。
sequenceDiagram
participant Evil as evil.example
participant Browser as ブラウザ
participant Bank as bank.example
Evil->>Browser: ① POST /transfer を bank へ送らせる
alt Lax または Strict
Browser->>Bank: ② Cookie を付けない
else None
Browser->>Bank: ② session_id を付ける
end
図: 別サイトからの POST にセッションIDが付くかは、SameSite で分かれる。
Strict
別サイトを起点にしたリクエストには、Cookie を付けません。リンクのクリック(トップレベルの GET)でも付けません。
検索結果やメールのリンクから銀行を開くと、最初の表示ではログイン済みに見えないことがあります。CSRF には強く、最初の1画面ではログイン状態が落ちて見えます。
Lax
別サイトからは、アドレスバーが切り替わるトップレベルの GET にだけ付けます。リンクをクリックしてページを開く移動です。別サイトの img や iframe、fetch の GET には付けません。別サイトからの POST にも付けません。
同じサイトからの POST には、Strict でも Lax でも付けます。止める対象は、別サイトを起点にしたリクエストです。
外部リンクの最初の表示でログイン状態が見え、別サイトからの POST には付かないので、認証 Cookie では Lax がよく使われます。未指定のときの既定も Lax です。
None
別サイトからでも Cookie を付けます。Secure が必須で、HTTPS のときだけ使えます。
Set-Cookie: session_id=s-1; SameSite=None; Secure
フロントと API が別のサイトにあるとき、外部サービスとの連携、iframe、一部の SSO で使います。Safari など、別サイトの Cookie を最初から受け取らないブラウザでは、None を付けても保存されないことがあります。
SPAで別ホストのAPIを呼ぶとき
画面が https://app.example.com、API が https://api.example.com だと、オリジンは違います。サイトは同じです。example.com のサブドメインで、スキームも https だからです。
SameSite の上では、この fetch は同じサイトです。Strict や Lax でも、API へのリクエストに Cookie を付けられます。SameSite=None が必要になるのは、https://app.example-a.com から https://api.example-b.com のように、サイト自体が違うときです。
sequenceDiagram
participant App as app.example.com
participant Browser as ブラウザ
participant API as api.example.com
App->>Browser: ① same-site の fetch
Browser->>API: ② ホストは違うが example.com<br/>スキームが同じなら Cookie を付ける
図: ホストは違っても example.com でスキームが同じなら same-site。Strict や Lax の Cookie が付く。
Cookie を預かるホストは API 側です。Domain を付けなければ、戻るのは api.example.com へのリクエストだけです。画面の app.example.com には付きません。セッションIDは、API が HttpOnly で預ければ足ります。
別オリジンへの fetch は、既定では Cookie を送りません。送るときは credentials を include にします。API は、呼び元のオリジンを Access-Control-Allow-Origin で指名し、Access-Control-Allow-Credentials に true を返します。オリジンの指定にアスタリスクは使えません。
fetch("https://api.example.com/account", {
credentials: "include",
});
| 値 | 別サイトから | 認証での位置 |
|---|---|---|
Strict | 付けない。リンクのクリックも含む | セキュリティ優先。最初の画面で未ログインに見えることがある |
Lax | トップレベルの GET だけ付ける。POST や埋め込みは付けない | 一般的な選択。未指定の既定 |
None | 付ける。Secure が必須 | 別サイトとの通信が必要なとき |
まとめ
- HTTP は前のリクエストを覚えない。Cookie は、ブラウザが名前と値を次のリクエストへ戻すことで、同じブラウザだとサーバーが分かるようにする仕組みです。
- ログインではセッションIDを預け、ユーザー情報はサーバー側に置きます。設定や流入元IDは、値そのものを預ける使い方です。
- バックエンドは
Set-Cookie、フロントはdocument.cookieで付けられます。ログイン用はHttpOnlyにするため、バックエンドから預けます。 - 自動送信の条件は、ホスト、
Path、期限、Secure、SameSiteです。HttpOnlyは送信そのものではなく、JavaScript から読めるかを決めます。 - 認証 Cookie は
SameSite=Laxがよく使われます。別サイトからの POST にセッションIDが付かないので、その形の CSRF を受けにくくします。画面と API が同じサイトのサブドメインなら、別オリジンでもStrictやLaxは付きます。