コンテンツにスキップ

Cookie を深く理解する — HTTP のステートレスを超える仕組み

はじめに

Cookie は Web 開発の基盤中の基盤だ。ログイン維持、カート管理、言語設定の保持——日常的に使う Web 機能のほとんどが Cookie に依存している。にもかかわらず「なんとなく分かっている」状態のエンジニアは多い。

この記事では、HTTP のステートレス性から出発して、Cookie がなぜ必要で、どう動き、どんな属性で守られているのかを体系的に整理する。EC サイトのカート、ログインのセッション管理、広告のサイト横断追跡という 3 つのユースケースを軸に、実務で必要な深さまで踏み込む。

なお、この記事の内容は Slidev で作成した発表資料(inbox/books/Presentation/cookie.md)をベースに、記事向けに再構成・加筆したものである。

背景・前提知識

HTTP はステートレス

HTTP の基本は「リクエストとレスポンスの 1 往復で完結する」こと。サーバーは 1 回の通信が終わると、そのブラウザのことを覚えていない。

ブラウザ → サーバー: GET /weather/tokyo
サーバー → ブラウザ: 200 OK + JSON

(ここで通信は完結。次のリクエストは「初対面」として扱われる)

これがステートレスの意味だ。レストランのレジで、会計のたびに店員が前の客を完全に忘れる状態に近い。

ステートレスの問題

ユーザーが求めるのはステートフルな体験——「さっきの続き」ができること——だ。

やりたいこと ステートレスだと
ログイン後にマイページを見る 「この人は誰?」になる
カートに商品を追加する 前の商品が消える
言語設定を日本語にする 次のページで英語に戻る

この問題を解決するのが Cookie だ。

本論

サーバーがブラウザに保存を指示する小さなテキストデータ。ブラウザはそれを保持し、同じサーバーへのリクエスト時に自動で送り返す。

# サーバー → ブラウザ(保存を指示)
Set-Cookie: cart_id=xyz123

# ブラウザ → サーバー(次回以降、自動で送り返す)
Cookie: cart_id=xyz123

重要なのは、ブラウザが勝手に作るのではなく、サーバーが Set-Cookie ヘッダーで発行するということ。Cookie はサーバーが選んだ情報をクライアント側に預ける仕組みだ。

Cookie はどこにでも送られるわけではない。

状況 送信される?
https://example.com で発行 → https://example.com にアクセス 送られる
https://example.com で発行 → https://other.com にアクセス 送られない
シークレットモードで保存 ウィンドウを閉じると消える

Amazon の Cookie が Google に送られないのは、この制限があるからだ。

種類 挙動 使い所
セッション Cookie ブラウザを閉じると消える 一時的なログイン、CSRF トークン
永続 Cookie Max-AgeExpires で指定した期限まで残る 言語設定、テーマ、ログイン維持

「ログインは残るのにシークレットモードでは消える」のは、この保存期間の違いで説明できる。


ユースケース 1: EC サイトのカート

ログインしていなくても、Cookie でそのブラウザのカートを識別できる。

流れ:

  1. 初回訪問: サーバーがカート識別子を発行 → Set-Cookie: cart_id=xyz
  2. 商品を追加: ブラウザが Cookie: cart_id=xyz を自動送信 → サーバーは xyz のカートに商品を追加
  3. 後日アクセス: 同じ Cookie から前回のカートを復元

ポイントは、Cookie に入っているのが「カートの中身そのもの」ではなく、カートを引くための ID であること。実際の商品一覧や数量はサーバー側で管理する。

EC サイトで使われる Cookie 属性:

属性 役割
Cookie 名と値 cart_id=xyz カートを引く識別子
Max-Age 2592000(30日) ブラウザを閉じてもカートを残す
Secure HTTPS 通信だけに限定
HttpOnly JavaScript から読めなくする

DevTools で確認する方法

  1. EC サイト(gihyo.jp など)で未ログインのまま商品をカートに入れる
  2. F12 → Network タブで Set-CookieCookie ヘッダーを確認する
  3. Application → Cookies で Cookie の名前・値・期限を確認する
  4. ブラウザを閉じて再訪問し、カートが残っているか確かめる

ユースケース 2: ログインとセッション管理

ログインの主役は認証とセッション管理。Cookie はその結果として発行された Session ID を運ぶ役 だ。

Cookie セッション
どこにある? ブラウザ側 サーバー側
役割 手がかりを運ぶ 本体データを持つ
典型例 session_id=abc abc → ログイン中, カート2件

ホテルで言うと、Cookie はカードキー、セッションはフロント側の宿泊台帳だ。

ログインの流れ

ブラウザ → サーバー: POST /login (認証情報)
サーバー: 認証成功 → セッション作成 → session_id=abc を発番
サーバー → ブラウザ: 302 /mypage + Set-Cookie: session_id=abc
ブラウザ: session_id=abc を保存
ブラウザ → サーバー: GET /mypage + Cookie: session_id=abc
サーバー: abc を照合 → ログイン済みと判断
サーバー → ブラウザ: 200 OK (マイページ)

POST /login の直後に 302 リダイレクトするのは、再読込時に「フォームを再送信しますか?」が出るのを防ぐため。Set-Cookie はリダイレクト応答でも保存される。

セッションの構造

セッションストア(サーバー側)

session_id=abc:
  user_id: 42
  role: member
  cart: [商品A, 商品B]
  expires: 2026-04-07 11:00

セッションには有効期限があり、一定時間操作がないとタイムアウトする。期限切れの Session ID が送られてもログアウト状態として扱う。

最小実装例(Node.js)

const sessions = new Map()

app.post('/login', (req, res) => {
  // 1. Session ID を作る
  const sessionId = crypto.randomUUID()

  // 2. サーバー側に状態を保存
  sessions.set(sessionId, { userId: 42 })

  // 3. Cookie に載せて返す
  res.cookie('session_id', sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
  })

  // 4. 画面は GET に移す
  res.redirect('/mypage')
})

Go での実装例

package main

import (
    "crypto/rand"
    "encoding/hex"
    "net/http"
    "sync"
    "time"
)

type Session struct {
    UserID    int
    ExpiresAt time.Time
}

var (
    sessions = make(map[string]*Session)
    mu       sync.RWMutex
)

func generateSessionID() string {
    b := make([]byte, 32)
    rand.Read(b)
    return hex.EncodeToString(b)
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
        return
    }

    // 認証処理(省略)

    sessionID := generateSessionID()

    mu.Lock()
    sessions[sessionID] = &Session{
        UserID:    42,
        ExpiresAt: time.Now().Add(24 * time.Hour),
    }
    mu.Unlock()

    http.SetCookie(w, &http.Cookie{
        Name:     "session_id",
        Value:    sessionID,
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteLaxMode,
        Path:     "/",
    })

    http.Redirect(w, r, "/mypage", http.StatusFound)
}

func mypageHandler(w http.ResponseWriter, r *http.Request) {
    cookie, err := r.Cookie("session_id")
    if err != nil {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    mu.RLock()
    session, ok := sessions[cookie.Value]
    mu.RUnlock()

    if !ok || time.Now().After(session.ExpiresAt) {
        http.Error(w, "Session Expired", http.StatusUnauthorized)
        return
    }

    // session.UserID でユーザー情報を取得して表示
}

ログアウトの流れ

ログアウトにはサーバー側のセッション破棄とブラウザ側の Cookie 削除の両方が必要。

Set-Cookie: session_id=; Max-Age=0

Max-Age=0 を受け取った時点で、ブラウザは Cookie を削除する。

属性 役割
Cookie 名 __Host-session_id=abc __Host- プレフィックスで安全性を強制
HttpOnly JavaScript から Session ID を隠す(XSS 対策)
Secure HTTPS のときだけ送信
SameSite Lax or Strict CSRF 攻撃を抑制

ユースケース 3: 広告とサイト横断追跡

「さっき見ていた商品が別サイトの広告に出る」——これも Cookie の仕組みだ。

ファーストパーティ サードパーティ
発行元 訪問中のサイト自身 別ドメイン(広告ネットワーク等)
用途 ログイン維持、カート サイト横断の追跡・広告配信

サイト横断追跡の仕組み

サイト A にアクセスした場合:

  1. サイト A のページに広告ネットワーク(ad-network)の広告枠がある
  2. ブラウザが ad-network に広告を読み込みに行く
  3. ad-network が Set-Cookie: user=xxx を返す(サードパーティ Cookie)

別のサイト B にアクセスした場合:

  1. サイト B にも同じ ad-network の広告枠がある
  2. ブラウザが Cookie: user=xxx を自動で送る
  3. ad-network は「この人はサイト A もサイト B も見た」と判明
  4. サイト A で見た商品に関連する広告を返す
ブラウザ 対応
Safari 2020 年〜 全面ブロック(ITP)
Firefox 2023 年〜 デフォルトブロック(ETP / Total Cookie Protection)
Chrome ユーザー選択制へ移行(強制的な廃止は見送り)

CHIPS(Cookies Having Independent Partitioned State)

2025 年末から主要ブラウザで利用可能になった新しい仕組み。サードパーティ Cookie を埋め込み先のサイトごとに分離する。

Set-Cookie: ad_id=xyz; SameSite=None; Secure; Partitioned

Partitioned 属性を付けると、同じ ad-network の Cookie でも、サイト A で保存されたものはサイト A からのアクセス時のみ送信される。サイト B では別の Cookie が使われるため、サイト横断の追跡ができなくなる。

要件: - SameSite=None が必須 - Secure が必須 - Chrome 114 以降で対応(Firefox は独自の Total Cookie Protection で同等機能を提供、Safari はサードパーティ Cookie 自体をブロック)


属性 効果 いつ使う
Secure HTTPS 通信時のみ送信 常に付ける
HttpOnly JavaScript からアクセス不可 Session ID など機密性の高い Cookie
SameSite=Lax 別サイトからの POST では送らない デフォルト(明示推奨)
SameSite=Strict 別サイトからは一切送らない 高セキュリティが必要な場面
SameSite=None クロスサイトでも送る(Secure 必須) サードパーティ用途
__Host- プレフィックス Secure + Path=/ + ドメイン指定なしを強制 ログイン Cookie
Partitioned 埋め込み先サイトごとに分離 CHIPS 対応のサードパーティ Cookie
Cookie 単体 Session ID と組み合わせ
theme=dark — テーマ設定 Session ID でサーバー側のログイン状態を引く
lang=ja — 言語設定 Session ID でカートの中身を引く
consent=yes — 同意状態 Session ID でユーザー権限を引く
cart_id=xyz — カート識別子

共通するのは、Cookie 自体に全情報を持たせるのではなく、サーバー側の状態を引くための手がかりを運ぶという発想だ。

実践・ユースケース

バックエンドエンジニアが押さえるべきチェックリスト

  1. ログイン Cookie には HttpOnly + Secure + SameSite=Lax を必ず付ける
  2. Session ID は暗号論的に安全な乱数で生成する(Go なら crypto/rand
  3. ログアウト時はサーバー側セッション破棄 + Max-Age=0 で Cookie 削除の両方を行う
  4. セッションにタイムアウトを設定する(無期限のセッションは危険)
  5. __Host- プレフィックスの使用を検討する(Cookie の安全要件を強制できる)
  1. F12 → Network タブでリロード
  2. 任意のリクエストをクリック → Headers → Set-Cookie / Cookie を確認
  3. Application → Storage → Cookies で名前・値・属性・期限を一覧確認
  4. ログイン前後で Cookie の増減を比較する

まとめ

  1. Cookie はサーバーが Set-Cookie でブラウザに保存させる小さなテキストデータ
  2. ブラウザは次回以降、発行元サイトへの対応リクエストでその Cookie を自動送信する
  3. Cookie 単体で設定値を持てるし、Session ID と組み合わせればサーバー側の状態も引ける
  4. EC のカート、ログイン維持、広告追跡はすべて同じ発想の延長線上にある
  5. セキュリティ属性(Secure, HttpOnly, SameSite)で Cookie を適切に守る
  6. サードパーティ Cookie は規制が進み、CHIPS(Partitioned が新たな選択肢になっている

関連する発表資料

このリポジトリには Slidev で作成した Cookie の発表スライドがある。

  • inbox/books/Presentation/cookie.md — Cookie・セッション・広告の仕組みを図解したスライド
  • inbox/books/Presentation/Web技術入門.md — URL・HTTP・Cookie を日常の例でつなげるスライド

ローカルで閲覧する場合:

cd inbox/books/Presentation
npm install
npm run dev
# http://localhost:3030 で表示される

参考