CookieとSessionの違いを説明できますか?

こんにちは、Webエンジニアのjuri-tです。

このタイトルの記事を読んでいる方は、おそらく違いが曖昧で困っている方でしょう。

Sessionとは

HTTPというプロトコルはステートレスなプロトコルです。要は状態を持たないってことですね。これはシンプルで割とうまくいくことも多いです。一方で、やっぱり状態を持ちたいというシチュエーションも往々にしてあります。たとえばログイン状態、はたまたショッピングカートなど。それを解決する手段として、リクエストを跨いだ状態をもつ仕組みがSessionです。こういう説明は、一度は聞いたことがあるのではないでしょうか?

Cookieとは

Cookieについて考えてみましょう。Cookieとは、ブラウザが持っているデータを保存できる領域のことです。ブラウザに好きなデータを保存できることで、ステートレスなHTTPにおいても状態をもつことができ、ログイン状態やショッピングカートといった機能が実現できます。

さて、違うものなのに同じような話ですね。じゃあ何が違うんだよ?と。そういう疑問を持つことは自然だと思います。

さて、私が最近得た一つの回答はこうです。

Sessionは仕様であり、Cookieは実装である

ただし、厳密にはちょっと違います。CookieはSession以外にも使われてます。

先日、responderについて初めてnoteにブログを書きました。ここの中の最後のほうにさらっと書いているんですが、responderではSessionに保存すると、Cookieに保存されます。

RailsやFlaskなど、他のWebアプリケーションでもCookieに保存することはできます。もちろんそれらの場合はmemcachedやredisやfile、DynamoDBなど多様なオプションがあります。要は、SessionというのはHTTPで状態を扱うための方法を抽象化した概念、仕様だといえます。そしてCoookieはその仕様を満たしている実装の一つと見ることができます。

Cookieについてもう少し詳しく

HTTPリクエストを送ると、HTTPレスポンスが返ってきます。その際、Headerやbodyに様々な情報を乗せることでWebの世界はできています。Cookieはその中の仕様として定められています。

Webサーバーはレスポンスを返すときに、以下の情報を含めます。

Set-Cookie: name=value;

このレスポンスを受け取ったブラウザは、このデータを保存しておき、次回同じサイトにアクセスしたときに、このCookieをCookieヘッダーとして一緒に送信します。

Cookie: name=value;

こうすることで、サーバーはHTTPリクエストのヘッダーから、前回保存したデータを受け取ることができ、状態を持つことが可能になっています。

responderでSessionを扱う

responderでは、Sessionは以下のように扱えます。

import responder

api = responder.API()


@api.route("/greeting")
async def greeting(req, resp):
   resp.text = f'Hello, {req.session["word"]}!'


@api.route("/okay")
async def okay(req, resp):
   resp.session['word'] = 'world'
   resp.text = 'OK!'


if __name__ == '__main__':
   api.run()

Sessionの扱いはどのフレームワークを使っても同じようなものだと思います。使うときはこれだけで使えますが、裏側ではSet-Cookieとしてレスポンスヘッダーに書き込んでくれたり、リクエストヘッダーからCookieを読み込んでくれています。便利ですね。

ブラウザからCookieを覗く

さて、ブラウザに保存されているCookieは見ることができます。見てみましょう。先程のプログラムを実行して、ブラウザでlocalhost:5042にアクセスし、F12を押してみてください。何も起こらなかった方はDeveloper Toolでググってください。以下はChromeでDeveloper Toolを開いた画像です。

さて、これまでの説明を聞くとNameにword、Valueにworldとして保存されているCookieがあるはずですがありませんね。さてなんででしょう?

Cookieは署名されている

responderでは、Responder-SessionのValueがsessionの値になっています。これは、Dictで持っているresp.sessionをシリアライズし署名しているデータです。暗号化されているわけではないです。確かめてみましょう。

CookieはJavaScriptでも読み書きできます。Developer Toolから、consoleを選んでdocument.cookieを実行してみましょう。

> document.cookie
"Responder-Session=eyJ0b2tlbiI6ICJhNjE2Yjk5MGM3ZWM0ZWI1YmYxNDlkOThjYzk1NDZjMCIsICJ1c2VybmFtZSI6ICJ3b3JsZCJ9.8BnBlVklLYncrwRbybl46tOTT10;" 

値を見ると、ピリオドが一つだけあるのがわかると思います。前半がSessionに書き込んだデータで、後半は署名です。前半をdecodeしてみましょう。

> window.atob("eyJ0b2tlbiI6ICJhNjE2Yjk5MGM3ZWM0ZWI1YmYxNDlkOThjYzk1NDZjMCIsICJ1c2VybmFtZSI6ICJ3b3JsZCJ9")
"{"token": "a616b990c7ec4eb5bf149d98cc9546c0", "username": "world"}"

ということで、sessionに保存したデータがCookieに保存されていることが確認できました。CookieはHTTPOnly属性をつけないと、JavaScriptからの読み書きができてしまいます。サーバー側で発行したCookieが好き勝手に書き換えられては困るので、署名をすることで正当性を検証しています。Sessionを使わないでCookieを使うことももちろん可能ですが、その場合はセキュリティのことをよく考えねばなりません。

さて、Sessionを使うと状態を持てることはわかったわけですが、その状態をすべてブラウザを共有している必要がないことも多いです。また、セキュアな状態を引き継ぎたいシチュエーションもあるでしょう。Cookieを使うとここまで見てきたように簡単に中身が見えます。こんな問題があるので、Sessionには識別子としてidだけもたせ、そのidに紐づくデータをサーバー側で管理するという手法が一般的です。

ではでは、今回はこのへんで。




サポートありがとうございます。頂いたご支援は美味しいものを食べに行きます。