◀ 16.

Python で Cookie を扱う|requests の Session と http.cookies

▶

PythonでCookieを扱うときは、jarに保持している値と実際に送る値、クライアントとサーバー、ブラウザの規則を分けて確認します。

Cookie jarへの保持と送信時のdomain、path、期限、Secureによる選別の違い

この記事の要点
  • Cookie を「送る側(クライアント)」なら requests.Session を使えば自動で保持される
  • 1 回だけ付けたいなら requests.get(url, cookies={"key": "value"})
  • 受け取った Cookie は res.cookies。属性まで見たいなら session.cookies を走査する
  • Cookie を「返す側(サーバー)」なら Set-Cookie ヘッダーを組み立てる。標準の http.cookies が使える
  • http.cookiejar.LWPCookieJar は平文の保存形式。ここでは架空値の保存・読み込みだけを検証し、実セッションの書き出しは勧めない

Session を使うのが基本

Cookie は「1 回のリクエストの設定」ではなく「やり取りをまたいで持ち回る状態」です。requests.Session を使うと、サーバーから返ってきた Cookie を自動で保存し、次のリクエストでは domain・path・期限・Secure などの条件に合う Cookie だけを送ります。保持と送信は別です。

注意:旧概念例:example.com はログイン用の実サービスではなく、認証成功を確認したコードではありません。実パスワードへ置き換えて実行したり、get_dict()で認証値を出力したりしないでください。HTTP成功だけでは認証成功を意味しません。実行する例は「ローカルで送信範囲を確かめる」を使用してください。

import requests

session = requests.Session()

# 1. ログイン。返ってきたセッション Cookie が session に保存される
session.post("https://example.com/login",
             data={"username": "taro", "password": "secret"},
             timeout=10)

# 2. 以降のリクエストには自動で付く
res = session.get("https://example.com/mypage", timeout=10)
print(res.status_code)

print(session.cookies.get_dict())   # {'sessionid': 'abc123'}
session.close()

独立した requests.get() 呼び出し間では Cookie jar は引き継がれません。ただし同じ呼び出し内のリダイレクトでは Cookie が利用されることがあります。認証方式やサーバーによって応答は異なり、必ず401やログイン画面になるとは限りません。

Cookie を明示的に設定する

注意:旧概念例:domain省略のsetや辞書はホスト限定ではありません。「以降すべて」は誤りで、通常のjarは送信条件で選別されます。urlも未定義です。直接Cookieヘッダーはjarの選別を迂回するため使わず、下記の架空値・domain/path指定例を使ってください。

import requests

# その 1 回だけ付ける
res = requests.get("https://example.com/",
                   cookies={"lang": "ja", "theme": "dark"},
                   timeout=10)

# Session に常設する(以降すべてのリクエストに付く)
session = requests.Session()
session.cookies.set("lang", "ja")
session.cookies.set("sessionid", "abc123",
                    domain="example.com", path="/")

# ヘッダーとして直接書くこともできるが、Session の管理から外れるので非推奨
res = requests.get(url, headers={"Cookie": "lang=ja; theme=dark"}, timeout=10)

Requests の手動設定で domain を省略すると、現在ホスト限定にはなりません。session.cookies.set("lang", "ja") や辞書由来の Cookie は空 domain で作られ、異なるホストにも送られ得ます。サーバーの Set-Cookie で Domain を省略したブラウザの host-only Cookie と混同しないでください。手動設定は domain と path を明示し、不要な共有を避けます。Python の既定ポリシーはブラウザのスコープ規則を完全再現しないため、秘密値を扱う場合は送信先も制限してください。

受け取った Cookie を読む

注意:旧診断例:Cookieの値や生ヘッダーを本番ログへ出さないでください。get_dict()は同名で異なるdomain/pathを失い、get(name)はCookieConflictErrorになり得ます。domain/pathを指定するか、値を出さず属性を走査します。Set-CookieのExpiresにはカンマがあるため文字列をカンマ分割しません。

res = requests.get("https://example.com/", timeout=10)

print(res.cookies.get_dict())        # そのレスポンスで設定されたものだけ
print(res.cookies.get("sessionid"))  # 無ければ None
print(res.headers.get("Set-Cookie")) # 生のヘッダー文字列

# 属性まで見たいときは 1 個ずつ走査する
for c in res.cookies:
    print(c.name, c.value, c.domain, c.path, c.secure, c.expires)
書き方返るもの
res.cookiesそのレスポンスで新しく設定された Cookie
session.cookiesこれまでに現在 jar 内に保持されている Cookie(今回送る対象と同じとは限らない)
res.request.headers.get("Cookie")最終リクエストで送信した Cookie ヘッダー。なければ None。値はログへ出さない

「送ったつもりが送られていない」場合は 3 番目を確認します。ドメインやパスが合っていないと、保持されていても送信対象から外れます。

削除と更新

注意:旧操作例の訂正:上書きの単位は名前だけでなくdomainとpathの組です。del jar[name]は同名Cookieを複数スコープから消し得ます。clear()はローカルの削除だけで、サーバーのセッション失効やログアウトを保証しません。clear(domain, path, name)で対象を限定してください。

session.cookies.set("lang", "en")          # 同じ名前なら上書き
del session.cookies["lang"]                # 1 個削除
session.cookies.clear()                    # 全削除(ログアウト相当)
session.cookies.clear("example.com")       # ドメイン単位で削除

print("lang" in session.cookies.get_dict())   # False

ログイン状態をファイルに保存する

注意:旧保存例は実行推奨ではありません。固定パスcookies.txt、存在確認後の読込み、ignore_discard=Trueで実セッションを平文保存する運用は避けてください。例はHTTPエラー確認・Sessionのclose・LoadError/OSError処理が不足しています。「架空値だけのLWP保存」で一時領域の限定検証を行います。

import http.cookiejar
import os
import requests

JAR_PATH = "cookies.txt"

jar = http.cookiejar.LWPCookieJar(JAR_PATH)
if os.path.exists(JAR_PATH):
    jar.load(ignore_discard=True)          # 前回の Cookie を復元

session = requests.Session()
session.cookies = jar                      # Session に差し込む

session.get("https://example.com/mypage", timeout=10)

jar.save(ignore_discard=True)              # セッション Cookie も含めて保存

discard=True のセッション Cookie は save/load の既定では除外されます。これを含めるには両方で ignore_discard=True が必要です。ignore_expires=False が既定で、期限切れは別条件として除外されます。Python のセッションはブラウザの終了イベントではなく jar の管理方針です。ファイルは暗号化されず、漏えいすれば権限を悪用され得ます。実際の認証 Cookie は本例へ持ち込まず、共有・ログ出力・Git登録・平文エクスポートをしないでください。

サーバー側で Cookie を返す

Web アプリを自作する場合は Set-Cookie ヘッダーを組み立てます。標準ライブラリの http.cookies が名前・値や属性の文字列表現を組み立てますが、HTTPレスポンスへの追加、認証、署名・暗号化、適切なセキュリティ方針は自動では実装しません。複数 Cookie は別々の Set-Cookie ヘッダーとして返し、カンマで分割・結合しないでください。

注意:この例はヘッダー文字列の生成・解析だけです。sessionid=abc123は架空値であり、予測可能なIDを認証に使えません。printだけではクライアントへ返りません。HttpOnly/SameSiteはブラウザの規則で、Pythonの値の参照を禁止しません。Secureは送信方式を制限する属性で、保存ファイルの暗号化ではありません。

from http.cookies import SimpleCookie

cookie = SimpleCookie()
cookie["sessionid"] = "abc123"
cookie["sessionid"]["path"] = "/"
cookie["sessionid"]["max-age"] = 3600
cookie["sessionid"]["httponly"] = True     # JavaScript から読めなくする
cookie["sessionid"]["secure"] = True       # HTTPS のときだけ送らせる
cookie["sessionid"]["samesite"] = "Lax"    # 別サイトからの送信を制限

print(cookie.output(header="Set-Cookie:"))
# Set-Cookie: sessionid=abc123; HttpOnly; Max-Age=3600; Path=/; SameSite=Lax; Secure

# 受け取ったヘッダーを解析する
incoming = SimpleCookie("lang=ja; theme=dark")
print(incoming["lang"].value)               # ja
属性意味推奨
HttpOnlyJavaScript から読めなくするブラウザの非HTTP API(document.cookie等)からの読取りを制限。Pythonからの読取りやXSS全体を防ぐものではない
SecureHTTPS のときだけ送る本番では付ける
SameSite他サイト由来のリクエストで送るかブラウザの cross-site 送信方針。Strict は外部からの遷移・決済復帰を妨げ得るため用途で選ぶ。Requests はブラウザの SameSite 制御を実装しない
Max-Age / Expires有効期限省略はセッション扱い。ブラウザのセッション復元で残る場合もある。Max-Age と Expires が両方あれば Max-Age が優先
Path / Domain送信対象の範囲ブラウザは Domain 省略なら host-only、指定すればサブドメインにも及ぶ。Path は送信条件でありアクセス制御ではない。Cookie はポートで分離されない

よくある詰まりどころ

  • Session を使っていない — 単発の requests.get() では Cookie が引き継がれない
  • リダイレクト先で消える — allow_redirects=False にして途中の Set-Cookie を確認する
  • ログインが JavaScript 経由 — フォーム POST ではなく API を叩いている。ブラウザの開発者ツールで実際のリクエストを見る
  • CSRF トークンが必要 — ログインページを一度 GET してトークンを取り出し、POST に含める
  • 日本語や記号が入る値 — 送受信側で合意した表現にする。quote は任意の値を自動復元する仕組みでも暗号化でもなく、SimpleCookie がURLデコードするわけではない

関連

ローカルで送信範囲を確かめる

以下は127.0.0.1の一時HTTPサーバーだけに接続する専用例です。実サイト・資格情報・外部ネットワークは使いません。架空のsidをルートと/appに分け、/applicationには/app用を送らないこと、無関係なDomainの拒否、Secureと期限切れの非送信を確認します。同名Cookieを辞書に平坦化せず、domain/pathで指定して更新・削除します。

HttpOnlyとSameSite=Strict付きのCookieもPythonから参照・送信できます。これはブラウザの制御が効かないことを示すだけで、ブラウザのSameSiteやCSRF対策を試験した結果ではありません。Secure付きはHTTPでは送られませんが、本例はTLS接続の検証をしません。実運用でTLS証明書検証を無効化しないでください。

次の3ファイルを同じディレクトリへ置きます。Requestsが導入済みの隔離環境でdemo.pyとpersistence.pyを実行してください。fixture.pyは認証サーバーではなく、固定した架空Cookieを発行して受信内容を返すだけです。timeoutとリダイレクト無効、環境プロキシ不使用により試験の範囲を絞っています。

fixture.py

"""Loopback-only synthetic Cookie fixture, not an authentication server."""

from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread


class Handler(BaseHTTPRequestHandler):
    def log_message(self, *args):
        pass

    def do_GET(self):
        self.send_response(200)
        if self.path == "/set":
            for value in (
                "sid=demo-root; Path=/; HttpOnly; SameSite=Strict",
                "sid=demo-app; Path=/app",
                "tls=demo-secure; Path=/; Secure",
                "durable=demo-persistent; Path=/; Max-Age=3600",
                "gone=demo-expired; Path=/; Max-Age=0",
                "foreign=demo-rejected; Domain=example.invalid; Path=/",
            ):
                self.send_header("Set-Cookie", value)
        data = self.headers.get("Cookie", "").encode("ascii")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)


@contextmanager
def loopback():
    server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
    server.daemon_threads = True
    thread = Thread(target=server.serve_forever, kwargs={"poll_interval": 0.05})
    thread.start()
    try:
        yield f"http://127.0.0.1:{server.server_port}"
    finally:
        server.shutdown()
        server.server_close()
        thread.join(timeout=2)
        if thread.is_alive():
            raise RuntimeError("Fixture failed to stop")

demo.py

"""Run from this directory: python -I -B -c ... (see README)."""

import requests
from requests.cookies import CookieConflictError

from fixture import loopback


def exercise(base):
    with requests.Session() as session:
        session.trust_env = False

        def get(path, **kwargs):
            response = session.get(base + path, timeout=(2, 2),
                                   allow_redirects=False, **kwargs)
            response.raise_for_status()
            return response

        first = get("/set")
        assert first.request.headers.get("Cookie") is None
        assert len(list(first.cookies)) == 4
        root = get("/echo")
        app = get("/app/echo")
        assert "sid=demo-root" in root.text
        assert "sid=demo-app" not in root.text
        assert "sid=demo-app" in app.text and "sid=demo-root" in app.text
        assert "sid=demo-app" not in get("/application").text
        for name in ("tls=", "gone=", "foreign="):
            assert name not in app.text
        assert len(list(root.cookies)) == 0
        assert len(list(session.cookies)) == 4
        assert session.cookies.get("sid", domain="127.0.0.1", path="/") == "demo-root"
        try:
            session.cookies.get("sid")
        except CookieConflictError:
            pass
        else:
            raise AssertionError("Ambiguous name must not silently select a value")

        assert "once=demo-once" in get("/echo", cookies={"once": "demo-once"}).text
        assert "once=" not in get("/echo").text
        session.cookies.set("kept", "demo-kept", domain="127.0.0.1", path="/")
        assert "kept=demo-kept" in get("/echo").text
        session.cookies.set("sid", "demo-updated", domain="127.0.0.1", path="/")
        session.cookies.clear("127.0.0.1", "/app", "sid")
        assert "sid=demo-updated" in get("/app/echo").text
        assert "sid=demo-app" not in get("/app/echo").text
        # No values or Cookie headers are logged, even for synthetic data.
        return "scope, duplicates, response jar and one-shot checks passed"


if __name__ == "__main__":
    with loopback() as url:
        print(exercise(url))

架空値だけのLWP保存

既定では永続Cookieだけ、明示した試験ではセッションCookieも保存し、load側でもignore_discardを指定します。ignore_expiresはFalseのままです。期限切れをTrueで復活させてもサーバーの認証有効性は復元されません。破損・アクセス不能時は処理を中止し、既存ファイルを空jarで上書きしません。

保存対象は専用fixtureの架空値だけです。専用一時ディレクトリを使い、POSIXではディレクトリ0700・ファイル0600と排他新規作成を使います。Windowsではchmodの数値をACLの保証とみなせないため、アクセス権を別途確認してください。本例は安全な実資格情報ストア・原子的な保存・クラッシュ復旧の実装ではなく、実セッションの平文保存を推奨しません。

persistence.py

"""Plaintext LWP serialization test: ONLY the local fixture's fake cookies."""

import os
from http.cookiejar import LWPCookieJar, LoadError
from pathlib import Path
from tempfile import TemporaryDirectory

import requests

from fixture import loopback


def load_checked(path, *, include_session=False):
    jar = LWPCookieJar(str(path))
    try:
        jar.load(ignore_discard=include_session, ignore_expires=False)
    except LoadError:
        raise RuntimeError("Invalid cookie fixture file; stop without overwriting") from None
    except OSError:
        raise RuntimeError("Cookie fixture file unavailable; stop") from None
    return jar


def save_new(jar, path, *, include_session=False):
    # Exclusive creation in a private temporary directory, never reuse a real jar.
    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    os.close(fd)
    try:
        jar.save(str(path), ignore_discard=include_session, ignore_expires=False)
    except OSError:
        raise RuntimeError("Cookie fixture save failed; stop") from None


def exercise_persistence(base):
    with TemporaryDirectory(prefix="cwiki-fake-cookies-") as directory:
        if os.name == "posix":
            os.chmod(directory, 0o700)
        jar = LWPCookieJar()
        with requests.Session() as session:
            session.trust_env = False
            session.cookies = jar
            response = session.get(base + "/set", timeout=(2, 2), allow_redirects=False)
            response.raise_for_status()
        durable = Path(directory) / "durable.lwp"
        all_fake = Path(directory) / "session-demo.lwp"
        save_new(jar, durable)
        save_new(jar, all_fake, include_session=True)
        assert {c.name for c in load_checked(durable)} == {"durable"}
        assert {c.name for c in load_checked(all_fake)} == {"durable"}
        restored = load_checked(all_fake, include_session=True)
        assert len(list(restored)) == 4
        assert all(not c.is_expired() for c in restored)
        return "LWP persistent/session round-trip checks passed"


if __name__ == "__main__":
    with loopback() as url:
        print(exercise_persistence(url))

値の設定と取得:SimpleCookie

元の最小例は from http import cookies の後に C = cookies.SimpleCookie()、C["flag"] = "0" と設定し、flag = C["flag"].value で値を取得するものです。これはPython内の文字列操作で、Requestsの送信jarとは別です。入力解析ではCookieErrorと必要キーの欠落も扱い、Cookieの値を認証済みの事実とみなしません。

公式資料と検証範囲

確認日:2026年10月26日。Requests Session、Requests Cookie実装、Python 3.12 http.cookiejar、Python 3.12 http.cookies、RFC 6265、MDN Set-Cookie。ブラウザ互換性、実認証、TLS、CSRF対策、OSのACLはこのローカル試験の保証対象ではありません。

子ページ

子ページはありません

同階層のページ
  1. 基本的なルール
  2. 変数
  3. 演算子
  4. 型
  5. 標準ライブラリ
  6. 外部ライブラリ
  7. 制御構文
  8. リスト(配列)
  9. タプル
  10. セット
  11. 辞書(dict)
  12. クラスとメソッド
  13. 継承の概念と必要性
  14. 継承の構文
  15. コンストラクタ
  16. cookieの値の設定と取得
  17. 例外処理
  18. 例外を文字列で出力する方法
  19. httpリクエスト(curl)をする方法
  20. Responseオブジェクトの中身の確認
  21. 変数が空かどうか判定する方法
  22. タイムゾーンの設定と現在日時の取得と文字列化
  23. シングルクォーテーションとダブルクォーテーションの違い

最近更新/作成されたページ