JWT vs セッション Cookie — どちらでログイン状態を保つべきか
Web アプリで JWT (ステートレストークン) とサーバー側セッション Cookie のどちらを採用するかを、サーバー側状態 / 失効処理 / スケーラビリティ / リスク の 4 軸で比較。CSRF と XSS の脅威モデルの違いも整理します。
4 つの判断軸 — 状態 / 失効 / スケール / 攻撃面
Web 認証の「JWT か Session Cookie か」という議論は、宗教戦争のように扱われがちですが、本質は 4 つのトレードオフに集約できます。サーバー側の状態保持 はステートレス (JWT) かステートフル (Session) かを決めます。失効のしやすさ はパスワード変更や不正利用が見つかったときに「いま発行済みのトークン」を即座に殺せるか、を左右します。スケーラビリティ は複数のサーバー / マイクロサービスで認証情報を共有するコストに直結します。攻撃面 は XSS と CSRF のどちらをより警戒するかを決めます。
「JWT がモダンで Session Cookie が古い」というのは誤った単純化です。Google も Amazon も社内サービスでは Session ベースの認証を併用しており、選び方は要件次第です。SPA + マイクロサービスなら JWT が自然、伝統的なモノリス Web アプリなら Session Cookie が素直、という具合に、システムのアーキテクチャと脅威モデルから逆算するのが正攻法です。
2 方式の比較表
| 項目 | JWT (Bearer Token) | Session Cookie |
|---|---|---|
| 状態保持 | ステートレス | ステートフル (サーバー側に保持) |
| 保存場所 | Authorization: Bearer ... ヘッダ / localStorage / Cookie | Cookie (HttpOnly; Secure; SameSite) |
| 状態の置き場 | クライアント (トークン自体に payload) | Redis / メモリ / DB |
| 即時失効 | 困難 (blocklist が必要) | 容易 (サーバーで削除) |
| トークンサイズ | 200-1000 バイト | 32-128 バイト (sid のみ) |
| 検証コスト | 署名検証のみ (DB アクセスなし) | DB / Redis ルックアップ必須 |
| 主な攻撃面 | XSS でトークン窃取、alg: none 攻撃 | CSRF (SameSite で緩和) |
| 必要インフラ | 秘密鍵 / 公開鍵のみ | セッションストア (Redis 等) |
| SPA / モバイル | 相性良 (Cookie 不要) | Cookie 設定が必要 |
| 複数ドメイン共有 | 容易 (Authorization ヘッダ) | CORS + SameSite=None; Secure 必要 |
サイズ感の目安として、Session Cookie の sid=abc123... は 32-64 文字程度ですが、JWT は header.payload.signature の 3 部構成で最小でも 200 バイト、ユーザー情報を詰め込むと 1KB を超えます。全リクエストにこのオーバーヘッドが乗る ため、API 設計では payload を最小化する判断が必要です。
アーキテクチャ別の推奨
1 サーバーの伝統的 Web アプリ (Rails / Django / Laravel): Session Cookie。実装が単純で、ログアウトボタンが「即時失効」を保証できます。フレームワーク標準のセッション機構をそのまま使えば、CSRF トークンも自動で挟まります。
マイクロサービス間 API 呼び出し: JWT。各サービスが公開鍵で署名を検証するだけで完結し、認証サーバーへの DB アクセスが不要になります。Auth0 / Cognito / Keycloak が発行する ID トークンもこのパターン。
SPA + REST API: 折衷案が定石。Access Token (短命 JWT、15 分) + Refresh Token (長命、HttpOnly Cookie) のペアにして、Access Token を Authorization ヘッダで送り、期限切れ時に Refresh Token で再発行する。XSS と即時失効の両方に対応できる。
モバイルアプリ: JWT。Cookie の自動送信はネイティブ HTTP クライアントでは扱いにくく、Authorization ヘッダで明示的に付けるほうがコードが直感的。
金融・医療など即時失効必須の業務: Session Cookie か、JWT + blocklist。「アカウント凍結ボタン」を押した瞬間にすべてのデバイスから即時ログアウトしたい要件では、ステートレスのメリットを捨ててもサーバー側状態を持つほうが安全。
ブラウザで検証して落とし穴を避ける
JWT を扱うときに最も多い事故は localStorage への保存。XSS が 1 箇所でも刺されば localStorage.getItem("token") でトークンが攻撃者に渡り、その後はサーバー側で何もできません。HttpOnly; Secure; SameSite=Strict の Cookie に入れれば JS から触れないため、XSS 耐性は段違いです。次に多いのが alg: none 攻撃 — 古い JWT ライブラリは header の alg を信用して検証をスキップする実装があり、攻撃者が {"alg":"none"} に書き換えた偽トークンを通してしまいます。
実装中の JWT が「本当に正しい payload を持っているか」「署名が正規発行か」を確認するときは、ペーストするだけで構造を見られるツールが必要です。jwt-decode は header と payload を base64url デコードして表示するだけのツールで、署名検証は行いません (デバッグ用途)。本番ロジックで使うべき jwt-verify は SubtleCrypto.verify で HS256 / RS256 / ES256 の署名を検証し、改ざんと有効期限を同時にチェックします。テスト用のトークンを新規発行したいときは jwt-encode で秘密鍵を入れて発行できます。いずれもトークンや秘密鍵は ブラウザ内で処理が完結し外部に送信されません。実装は GitHub で公開しており、DevTools の Network タブで送信ゼロを目視できます。本番の秘密鍵を貼って動作確認するときも、コードが手元にある安心感が効きます。