ステートレスとCookie — なぜログイン状態を覚えておく仕組みが必要なのか

バイブコーディング入門 — 第10回

前回までのおさらいと、今回のテーマ

前回(第9回)では、HTTPヘッダを学んだ。封筒に書かれた付帯情報として、Content-Type(データの形式)とAuthorization(認証トークン)を中心に解説した。

ここまでのシリーズで、HTTP通信の主要な要素を一通り学んできた。

  • 第5回: URL(手紙の宛先)
  • 第7回: HTTPメソッド(手紙の用件 — GET・POST・PUT・DELETE)
  • 第8回: ステータスコード(返事のスタンプ — 200・404・500 など)
  • 第9回: HTTPヘッダ(封筒の付帯情報 — Content-Type・Authorization)

今回は、これまで触れてこなかったHTTPの根本的な特性に切り込む。それが「ステートレス」だ。

ステートレスとは「状態を持たない」という意味だ。HTTPは、リクエストとレスポンスの1往復が終わると、その通信のことを一切覚えていない。たとえあなたが1秒前にログインしたとしても、次のリクエストでは「初めまして、どちら様ですか」と聞き返されてしまう。

これは不便に思える。でも、Webはそういう設計で作られている。そして、その不便さを補うために、Cookie・セッション・JWTという仕組みが生まれた。

今回は、この「記憶を持たないWeb」の仕組みと、記憶を補う3つの方法を解説する。

ステートレスとは — 毎回「初対面」のHTTP

ステートレス = リクエストごとに記憶がリセットされる

図: ステートレス = リクエストごとに記憶がリセットされる

手紙の比喩に戻そう。

あなたは郵便局の窓口に行って、手紙を出す。窓口の担当者は手紙を受け取り、適切に処理して、返事をくれる。ここまでは問題ない。

しかし、この窓口の担当者は特殊だ。1通の手紙を処理し終えると、その瞬間に記憶がすべて消える。あなたが2通目の手紙を持って窓口に行くと、担当者は「初めまして」と言う。1分前にあなたの手紙を処理したばかりなのに、まったく覚えていない。

これがHTTPのステートレスという特性だ。ステート(state)は「状態」、レス(less)は「ない」。つまり「状態を持たない」。

なぜこんな設計になっているのか。理由はシンプルで、Webの規模で考えると、この方が効率的だからだ。1つのWebサイトに同時に何万人もアクセスする状況を想像してほしい。もしサーバーが全員の状態を記憶し続けなければならないとしたら、膨大なメモリが必要になり、サーバーの負荷が一気に上がる。「毎回忘れる」ほうが、サーバーにとっては身軽で処理しやすい。

しかし、ユーザーにとっては困る。ログインしたのに、次のページに移動したら「ログインしていません」と言われる。商品をカートに入れたのに、決済ページに進んだらカートが空になっている。これでは使い物にならない。

そこで、「ステートレスなHTTPの上に、状態を保持する仕組みを載せる」という解決策が考えられた。その代表的な方法がCookie、セッション、JWTだ。

Cookie(クッキー)は、サーバーがブラウザに預ける小さなデータだ。

手紙の比喩で説明しよう。あなたが初めて窓口に行ったとき、担当者はあなたの名前を確認して、「この名札を付けてください」と渡す。次にあなたが窓口に来たとき、担当者は記憶を失っているが、あなたの名札を見て「田中さんですね」と認識できる。

Cookieの仕組みは以下の通りだ。

  1. ブラウザが初めてサーバーにリクエストを送る
  2. サーバーがレスポンスを返すとき、Set-Cookie というヘッダで「この情報を預かっておいて」とブラウザに伝える
  3. ブラウザはその情報を保存する
  4. 以降、同じサーバーにリクエストを送るたびに、ブラウザが自動的にCookieヘッダとしてその情報を送り返す

ここで重要なのは「自動的に」という部分だ。あなたが何かする必要はない。ブラウザが勝手にCookieを保存し、勝手に送り返してくれる。開発者がコードで明示的に送信処理を書く必要もない(ほとんどの場合)。

前回学んだHTTPヘッダの中に、CookieとSet-Cookieがあったことを覚えているだろうか。Set-Cookieはレスポンスヘッダ(サーバー→ブラウザ)で「預けるとき」に使い、Cookieはリクエストヘッダ(ブラウザ→サーバー)で「送り返すとき」に使う。

Cookieの中身

Cookieに保存される情報は「名前=値」のペアだ。たとえば、こんな形になる。

session_id=abc123def456

この例では、名前が session_id、値が abc123def456 だ。この値だけを見ても、何の意味があるのかは分からない。これはわざとそうなっている。理由は次のセクションで説明する。

セッション — サーバー側で管理する「来客名簿」

Cookieだけでは、ログイン状態を管理するには不十分だ。なぜなら、Cookieはブラウザ側に保存されるデータだからだ。もしCookieに「ユーザー名: 田中太郎、権限: 管理者」と書いてあったら、悪意ある人がその情報を書き換えて「ユーザー名: 山田花子、権限: スーパー管理者」とすることもできてしまう。

そこで考えられたのがセッション(session)という仕組みだ。

Cookie + セッションの仕組み

図: Cookie + セッションの仕組み

手紙の比喩で言えば、窓口の担当者は記憶を失うが、脇に「来客名簿」が置いてある。名簿には「名札番号123 → 田中太郎、管理者」と書かれている。あなたが名札(Cookie)を見せると、担当者は名簿を引いて「田中さんですね」と確認できる。

この仕組みのポイントは3つだ。

1つ目。ブラウザ側のCookieにはセッションIDだけが入っている。名前や権限などの重要な情報はCookieには入れない。意味のない長い文字列(abc123def456のようなランダムな値)だけだ。

2つ目。実際のユーザー情報はサーバー側のセッションストア(来客名簿)に保存されている。データベースやメモリの中に「このセッションIDは、この人のもの」という対応表がある。

3つ目。ブラウザがCookieを送るたびに、サーバーはセッションIDをキー(鍵)にしてセッションストアを検索し、対応するユーザー情報を見つける。

この方式は長く使われてきた。今でも多くのWebアプリがセッション方式を採用している。ただし、サーバー側にセッションストアを持つ必要があるため、サーバーの数が増えると管理が複雑になるという課題がある。

JWT — 名札自体に情報が書いてある方式

JWT(ジェイダブリューティー、JSON Web Token)は、セッションとは異なるアプローチだ。

セッション方式では、Cookieには意味のないIDだけを入れ、実際の情報はサーバー側に保存していた。JWT方式では、トークン(名札)自体にユーザー情報が含まれている。

手紙の比喩に戻ろう。セッション方式では、名札には番号だけが書いてあり、来客名簿と照合する必要があった。JWT方式では、名札に「田中太郎、管理者、有効期限: 2026年12月31日」と直接書いてある。担当者は名簿を引く必要がなく、名札を読むだけで本人確認ができる。

しかし、名札に直接情報が書いてあるなら、書き換えられる危険があるのでは。セッションのときと同じ問題だ。

JWTはこの問題を「署名」で解決している。名札の情報は誰でも読めるが、名札の末尾にサーバーだけが作れる署名が付いている。もし誰かが名札の情報を書き換えると、署名と内容が一致しなくなるため、サーバーは「この名札は改ざんされている」と判断して拒否する。

前回学んだAuthorizationヘッダに出てきた Bearer eyJhbGciOiJIUzI1NiIs... という長い文字列。あれがJWTだ。eyJ で始まる文字列を見たら、「JWTだな」と認識できる。

JWTの構造

JWTは3つの部分がドット(.)で区切られている。

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjN9.HMAC_SIGNATURE

1つ目(ヘッダ): 署名のアルゴリズム(暗号化の方法)が書かれている。

2つ目(ペイロード): ユーザーIDや有効期限などの実際の情報が書かれている。Base64(ベースロクヨン)という方式でエンコード(変換)されているだけで、暗号化はされていない。つまり、誰でも読める。

3つ目(署名): サーバーの秘密鍵(サーバーだけが知っている合言葉)で作られた署名。これにより改ざんを検知する。

暗号化されていないことに不安を感じるかもしれないが、JWTの目的は「内容を隠すこと」ではなく「改ざんを検知すること」だ。パスワードなどの機密情報はJWTに入れない。通信自体はHTTPS(暗号化された通信)で保護されているので、途中で第三者に読まれることはない。

Cookie + セッション と JWT の比較

セッション方式 と JWT方式 の比較

図: セッション方式 と JWT方式 の比較

どちらが良い・悪いという話ではない。用途やアプリの規模によって使い分けられている。

バイブコーディングで使うことが多いSupabase(スーパーベース)は、JWT方式を採用している。Supabaseでログインすると、JWTが発行され、以降のAPIリクエストにそのJWTが自動的に付与される。前回学んだ Authorization: Bearer ... の Bearer の後ろに入っているのが、まさにこのJWTだ。

一方、従来型のWebフレームワーク(Ruby on RailsやPHP、Djangoなど)では、Cookie + セッション方式が標準的だ。

DevToolsでCookieを確認する方法

DevToolsでCookieを確認する方法を紹介する。今回はNetworkタブではなく、Applicationタブを使う。

ステップ1: DevToolsを開く

F12キー(Macの場合は Command + Option + I)を押してDevToolsを開く。

ステップ2: Applicationタブを選ぶ

上部のタブから「Application」を選ぶ。表示されていない場合は「>>」をクリックして隠れているタブを表示する。

ステップ3: Cookieを確認する

左側のメニューから「Cookies」を開くと、サイトのドメインが表示される。ドメインをクリックすると、そのサイトに保存されているCookieの一覧が表示される。

各Cookieには以下の情報がある。

  • Name: Cookieの名前(session_id、sb-access-token など)
  • Value: Cookieの値(セッションIDやJWTの文字列)
  • Domain: このCookieを送信する対象のドメイン
  • Expires: 有効期限。「Session」と表示されている場合、ブラウザを閉じると消える
  • HttpOnly: チェックが入っている場合、JavaScript(プログラム言語)からアクセスできない。セキュリティのための設定
  • Secure: チェックが入っている場合、HTTPS通信のときだけ送信される

Supabaseを使ったアプリの場合、sb-access-token や sb-refresh-token という名前のCookieが見つかるかもしれない。これらがJWTだ。

ステップ4: JWTの中身を見る(任意)

JWTの中身を確認したい場合は、jwt.io というWebサイトが便利だ。Cookieの値をコピーして jwt.io に貼り付けると、ヘッダ・ペイロード・署名がデコード(復元)されて読める形で表示される。ユーザーIDや有効期限が確認できる。

注意点として、jwt.io に貼り付けるのは自分の開発環境のトークンだけにすること。本番環境のトークンを外部サイトに貼り付けるのはセキュリティ上避けたほうがよい。

バイブコーディングでの遭遇パターン

ステートレスとCookieに関連して、バイブコーディングでよく遭遇する問題を紹介する。

パターン1: ログイン後に画面遷移すると未ログインになる

ログイン画面でメールアドレスとパスワードを入力して、ログインに成功する。しかし、マイページに移動すると「ログインしてください」と表示される。

これはまさにステートレスの問題だ。ログインのリクエストは成功したが、その結果(トークンやCookie)が正しく保存されていないか、次のリクエストに正しく付与されていない。

DevToolsのApplicationタブでCookieを確認する。ログイン後にCookieが保存されているか。NetworkタブでマイページのAPIリクエストにAuthorizationヘッダやCookieが含まれているか。

パターン2: しばらく操作しないとログアウトされる

昼休みにパソコンから離れて戻ってくると、ログアウトされている。これはセッションやJWTの有効期限が切れたためだ。

JWT方式の場合、トークンには有効期限がある。期限が切れると、サーバーはそのトークンを受け付けなくなる(401が返る)。通常は「リフレッシュトークン」という仕組みで自動的に新しいトークンを取得するが、その処理が正しく動いていないとログアウトされてしまう。

パターン3: 別のタブでログアウトすると、元のタブも使えなくなる

タブAでアプリを使いながら、タブBでログアウトした。タブAに戻ってボタンを押すと、エラーになる。

Cookieはブラウザ全体で共有される。タブBでログアウトしてCookieが削除されると、タブAからのリクエストにもCookieが付かなくなる。

パターン4: ページリロードするとログイン状態が消える

ログイン中にブラウザのリロードボタンを押すと、未ログイン状態に戻ってしまう。これはトークンの保存場所に問題がある可能性が高い。

トークンをJavaScriptの変数(プログラムの一時的な記憶)にだけ保存していると、ページをリロードした瞬間にその変数は消えてしまう。CookieやlocalStorage(ブラウザの永続的な記憶領域)に保存する必要がある。

AIへの伝え方テンプレート

ログイン状態に関する問題をAIに伝えるときのテンプレートを紹介する。


(症状)ログインに成功した後、マイページに遷移すると未ログイン状態になる。 (DevToolsの確認1)ログインAPI(POST /api/auth/login)は200を返している。レスポンスにトークンが含まれている。 (DevToolsの確認2)マイページのAPI(GET /api/profile)は401を返している。 (Cookieの確認)ApplicationタブのCookieにセッション情報やトークンが保存されていない(または、保存されているが次のリクエストに含まれていない)。


もう1つのテンプレート。


(症状)ログイン状態が一定時間で切れてしまう。リロードすると未ログインになることもある。 (DevToolsの情報)APIリクエストが401を返し始めるタイミングがある。 (確認したこと)ApplicationタブでCookieの有効期限を確認したところ、Expiresが「Session」になっている。 (質問)トークンのリフレッシュ処理は実装されていますか。トークンの保存方法と有効期限の設定を確認してもらえますか。


よくある不安と答え

Cookieは危険なものではないか

「Cookieを許可しますか」というポップアップを見て、なんとなく怖い印象を持っている人もいるかもしれない。Cookieそのものは、サーバーがブラウザに預ける小さなデータにすぎない。ウイルスでもなければ、パソコンの中を覗き見るものでもない。ポップアップで聞かれているのは、主に広告やアクセス解析のためのCookie(トラッキングCookie)の許可であり、ログインに必要なCookieとは別物だ。

JWTの中身が誰でも読めるなら危なくないか

JWTのペイロード部分は、エンコードされているだけで暗号化されていないため、確かに誰でも読める。だからこそ、パスワードやクレジットカード番号などの機密情報はJWTに入れない。入っているのはユーザーIDや有効期限など、読まれても直接的な被害がない情報だ。通信自体はHTTPSで暗号化されているため、途中で第三者に傍受される心配もない。

セッションとJWT、どちらを選ぶべきか

バイブコーディングでは、使うフレームワークやサービスが採用している方式をそのまま使えばよい。Supabaseを使うならJWT、Ruby on Railsを使うならセッション。自分で選ぶ必要はほとんどない。もしAIに認証機能を作ってもらうときは、「Supabaseの認証機能を使ってログイン機能を実装して」と伝えれば、AIが適切な方式で実装してくれる。

Cookieが消えたらどうなるか

ブラウザのCookieを削除すると、ログイン状態が失われる。次にアクセスしたときは、再度ログインが必要になる。これは正常な動作だ。Cookieを消すことは「名札を外す」のと同じで、サーバーはあなたが誰か分からなくなる。

ログイン状態の管理は自分でコードを書くのか

バイブコーディングでは、認証の仕組みはSupabase AuthやNextAuth.js(ネクストオースジェイエス)などのライブラリ(既製のプログラム部品)が提供してくれる。Cookie、セッション、JWTの処理はこれらのライブラリが内部で行っている。あなたが直接コードを書く必要はない。今回の知識は「エラーが起きたとき、何が起きているかを理解し、AIに的確に伝える」ために使うものだ。

まとめ

HTTPはステートレス — リクエスト間の記憶を持たない。ログインしても、次のリクエストでは「初対面」に戻る。この問題を解決するために、3つの仕組みが使われている。Cookie(サーバーがブラウザに預ける名札)、セッション(サーバー側の来客名簿。Cookieには名簿の番号だけ入れる)、JWT(名札自体に情報が書いてあり、署名で改ざんを防ぐ)。Supabaseを使ったバイブコーディングではJWT方式が標準だ。ログイン関連のエラーに遭遇したら、ApplicationタブでCookieの状態を確認し、NetworkタブでAuthorizationヘッダやCookieの送信状況を確認する。この2つの情報をAIに伝えれば、問題の原因が特定しやすくなる。

次回は「CORSエラー — なぜ『別のサーバー』にアクセスできないのか」。バイブコーディングで頻繁に遭遇するCORSエラーの仕組みと解決方法を解説する。

参考リファレンス

  • MDN Web Docs「HTTP Cookie の使用」— Mozilla が提供するCookieの技術リファレンス
  • MDN Web Docs「HTTP の概要 — HTTP はステートレス」— ステートレスの公式解説
  • jwt.io — JWTのデコード・検証ができるWebツール
  • 前回: 第9回「HTTPヘッダの基本 — Content-TypeとAuthorizationで何をやりとりしているか」(/articles/vc-009)
  • 次回: 第11回「CORSエラー — なぜ『別のサーバー』にアクセスできないのか」(/articles/vc-011)
  • バイブコーディング入門 カリキュラム(/vibe-coding)