ウェブ開発を学んでいると、「HTTP はステートレス、つまり状態を持たないプロトコルである」という一文に必ず出くわす。ところが現実のウェブサイトでは、一度ログインすればページを移っても自分だと認識される。ショッピングカートの中身も、ちゃんと残っている。
状態を持たないはずの HTTP の上で、ログイン状態という紛れもない〈状態〉をどうやって実現しているのか。HTTP を役所の窓口係にたとえるなら、彼は筋金入りの記憶喪失である。五秒前に会った客のことすら覚えていない。いささか矛盾めいたこの話を、改めて整理してみた。
HTTP の外にある状態
種明かしをしてしまうと、状態は HTTP の外で管理されている。HTTP というプロトコル自体に状態を持たせるのではない。サーバー側で状態を保持し、その識別子だけをリクエストのたびにやり取りするのだ。そうして、擬似的にステートフルな振る舞いを作り出している。
流れを追うと、次のようになる。まず、ログインに成功した時点で、サーバーは「この人は誰々さんだ」という情報をセッションとして保存する。置き場所はメモリやデータベースで、それを指し示す一意のセッション ID も発行する。
続いて、サーバーはレスポンスヘッダーを使い、その ID をクライアントであるブラウザに保存させる。たいていは Cookie の出番である。以降、クライアントはリクエストを送るたびに、その ID を自動的に添えてくる。
最後に、サーバーは届いた ID をもとに、保存しておいたセッション情報を引き出す。そして「ああ、ログイン済みのユーザーだな」と判断する。窓口係は客の顔を覚えていないが、手元の台帳と番号札を照らし合わせれば用は足りるわけだ。
セッションとトークン
ログイン状態の管理方法は、大きくセッション方式とトークン方式の二つに分かれる。もっとも伝統的で一般的なのは、セッションと Cookie の組み合わせだ。ユーザーの情報は、サーバー側のデータベースや Redis などに保持する。Cookie に入れておくのは、その情報を引き出すための鍵、すなわちセッション ID だけである。
サーバーが状態を握っているので、サーバー側のセッションを消せばログアウトは確実に成立する。台帳から名前を消してしまえば、番号札はただの紙切れだ。その代わり、セッション情報をしまっておくための DB や Redis といったストレージが別途必要になる。
一方、モダンな Web API やシングルページアプリケーション(SPA)でよく使われるのが、JWT(JSON Web Token)に代表されるトークン方式である。こちらはユーザー情報そのものを署名付きのトークンに詰め込み、クライアント側に持たせる。サーバーは届いたトークンの署名が正しいかを検証するだけでよく、手元にデータを置いておく必要がない。いわば台帳を捨てて、客に署名入りの身分証を持ち歩かせる方式である。
サーバーが状態を持たない完全なステートレスなので、スケールしやすいのが利点だ。窓口係を何人に増やしても、台帳を共有する手間がない。もっとも、一度発行したトークンをサーバー側から強制的に無効化しにくいという課題は残る。客のポケットにある身分証を、窓口から取り上げる術はないのだ。
ステートレスでも成り立つ理由
要するに、HTTP はステートレスだが、アプリケーションはステートフルに振る舞える、ということだ。HTTP がステートレスだというのは、プロトコル上の制約に過ぎない。各リクエストが前のリクエストを覚えておく必要はない、と言っているだけである。リクエストヘッダーに識別子を含めておけば、アプリケーション層、つまりサーバー側のプログラムが過去の文脈を復元するのはまったく自由なのである。
たとえるなら、毎回のリクエストで自己紹介カード(Cookie やトークン)を差し出しているようなものだ。窓口係は相変わらず、五秒前に会ったことすら覚えていない。それでも、毎回カードを見せさえすれば話は通じる。HTTP 自体が記憶を持たなくてもやり取りが成立するのは、そういう理屈である。
以上、HTTP がステートレスなのにログイン状態を維持できる理由を整理した。ログイン状態の維持は、HTTP を拡張して実現しているのではない。外部ストレージと識別子の受け渡しという、アプリケーション側の工夫によって成り立っている。
原則を知り、その限界をどう補っているかを知る。それが、セキュアでスケーラブルなウェブサイトを設計する第一歩になる。記憶を持たない相手とは、名乗り続ければよいのだ。