Yuki's Tech Blog

仕事で得た知見や勉強した技術を書きます。

クッキーとセッション、セッション管理についてまとめる

目次

概要

クッキーとセッション、セッション管理を雰囲気で使っていたので、ちゃんと理解したいなと思い、記事にまとめました。

クッキーとは

まずはRFCにおけるクッキーの定義を見てみましょう。

triple-underscore.github.io

RFCはインターネット技術における法律をまとめたドキュメントのようなものです。

  1. 序論

この文書は、 HTTP の Cookie と Set-Cookie ヘッダを定義する。 HTTP サーバは、 Set-Cookie ヘッダを利用して,クッキーと呼ばれる[ ( 名前, 値 ) が成すペアと, それに結び付けられたメタデータ ]を UA に渡すことができる。 UA は、サーバへ後続の要請を為す際に,そのメタデータと他の情報を利用して ( 名前, 値 ) が成すペアを Cookie ヘッダ内に返すかどうか決定する。

Set-Cookieヘッダについての項目も見てみましょう。

4.1.1. 構文

略式的には、 Set-Cookie 応答ヘッダは,次の並びで与えられる :

ヘッダ名 "Set-Cookie",

文字 %x3A( ":" ),

1 個のクッキー

ここで、クッキーは,次の並びで与えられる: ( クッキーの名前( cookie-name ), クッキーの値( cookie-value ) ) が成すペア( cookie-pair ) 0 個以上の属性( cookie-av ) — 各 属性は、 ( 属性名, 属性値 ) が成すペアを与える サーバは、次の文法に適合しない Set-Cookie ヘッダを送信するべきでない:

Set-Cookieヘッダにおけるもう一つの項目も見てみましょう。

4.1.2. 意味論(規範的でない)

この節では、 Set-Cookie ヘッダの意味論を単純化して述べる。 これらの意味論は、サーバにおけるクッキーの最もよくある利用を理解するには十分詳細なものである。 全部的な意味論は § UA 要件 にて述べる。 Set-Cookie ヘッダを受信した UA は、そのクッキーを,その属性もひっくるめて格納する。 UA は、後続して HTTP 要請を為す際には,適用可能, かつ まだ失効していないクッキーを Cookie ヘッダに内包する。 UA は、すでに格納されたクッキーと同じ[ cookie-name, domain-value, path-value ]を伴う新たなクッキーを受信した場合、既存のクッキーは抹消され,新たなクッキーに置換される。 サーバは、新たなクッキーを — その Expires 属性の値を過去に設定した上で — UA に送信することにより,クッキーを削除できることに注意。 クッキーの属性により指示されない限り、クッキーは、(例えば,下位ドメインではなく)生成元サーバに対してのみ返され,現在のセッションの終了時に失効する( “セッションの終了” は、 UA により定義される)。 UA は、認識できないクッキー属性は,無視する(クッキーまるごと,ではなく)。

次はCookieヘッダについてみていきましょう

UA は、自身が格納したクッキーたちを Cookie ヘッダに伴わせて,生成元サーバへ送信する。 サーバが § Set-Cookie ヘッダ の要件に適合する(かつ UA が § UA 要件 に適合する)場合、 UA は,次の文法に適合する Cookie ヘッダを送信することになる:

Cookieヘッダの意味論についても見てみましょう。

4.2.2. 意味論

cookie-pair が,UA に格納されているクッキーを表現する。 cookie-pair は、 UA が Set-Cookie ヘッダ内に受信した[ cookie-name と cookie-value ]を包含する。 クッキーの属性は、 UA からサーバへは返されないことに注意。 特に,サーバは、 Cookie ヘッダ単独からは,クッキーが[ いつ失効するのか?/ どのホストに有効なのか?/ どのパスに有効なのか?/ Secure や HttpOnly 属性を伴って設定されたものかどうか? ]を決定できない。 Cookie ヘッダ内の個々のクッキーの意味論については、この文書では定義されない。 これらのクッキーに対する応用に特有な意味論は、サーバごとに指定されることが期待されている。 クッキーは Cookie ヘッダにて直列化されるが、サーバは,その直列化の順序に依拠するべきでない。 特に, Cookie ヘッダが同じ名前の 2 個のクッキーを包含している場合(例えば、異なる[ Path / Domain ]属性を伴って設定されたもの)、サーバは,これらのクッキーがヘッダ内に現れる順序に依拠するべきでない。

この引用文を見る限り、クッキーとは、(クッキーの名前, クッキーの値)が成すペアと、それに結び付けられた0個以上のメタデータであることがわかります。

クッキーがどのようにサーバーから送られるのかを見てみる

クッキーは、サーバーがクライアント(ブラウザ)に送るHTTPレスポンスメッセージを通して、クライアント(ブラウザ)に送られ保存されます。

例えばログインフォームでユーザー認証をした場合を考えます。 サーバーサイドでは以下のプログラムを使用します。

github.com

クライアント(ブラウザ)にはChromeを使用します。

ログインフォームで名前とemailを入力してログインボタンを押すと、以下のようなHTTPリクエストメッセージがサーバーに送られます。

POST /login HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 75
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/login
Accept-Encoding: gzip, deflate, br
Accept-Language: ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6,zh;q=0.5

email=yukihoge%40gmail.com&password=yukihoge&submit_name=%E9%80%81%E4%BF%A1

そして、サーバーはこのHTTPリクエストメッセージを解釈して処理をして、以下のようなHTTPレスポンスメッセージを返します。 このHTTPレスポンスメッセージにSet-Cookieというヘッダがあります。このヘッダにはsession_idという名前のクッキーが、Set-Cookieヘッダのvalueとして指定されています。このSet-CookieヘッダをHTTPレスポンスメッセージに含めることで、このHTTPレスポンスメッセージを受け取ったクライアント(ブラウザ)はSet-Cookieヘッダに指定してあるクッキーを保存します。

HTTP/1.1 302 Found
Date: Fri, 08 Sep 2023 06:30:46
Server: HenaGoServer/0.1
Content-Length: 0
Content-Type: text/html; charset=UTF-8
Connection: Close
Set-Cookie: session_id=c1c3e7d8-f991-4149-80ce-a2bfade4d5aa; HttpOnly
Location: /mypage

ブラウザのCookiesタブを見ると、ちゃんと送信したクッキーが保存されていることが分かります。

Image from Gyazo

そして上のHTTPレスポンスメッセージではリダイレクトを指示しているので、クライアント(ブラウザ)はLocationに指定してあるパスに対してHTTPリクエストを出します。以下がそのリクエストの際にサーバーに送られたHTTPリクエストメッセージです。

GET /mypage HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
sec-ch-ua: "Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Referer: http://localhost:8080/login
Accept-Encoding: gzip, deflate, br
Accept-Language: ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6,zh;q=0.5
Cookie: session_id=c1c3e7d8-f991-4149-80ce-a2bfade4d5aa

Cookieヘッダを使って、ブラウザに保存したクッキーをサーバーに送信していることが分かります。クッキー自体に特に制限がかけられていなければ、ブラウザはクッキーを生成したサーバーに対してリクエストする際には、必ずCookieヘッダを使ってクッキーも一緒に送信します。

その後以下のHTTPレスポンスメッセージがサーバーから返されます。

HTTP/1.1 200 OK
Date: Fri, 08 Sep 2023 06:48:01
Server: HenaGoServer/0.1
Content-Length: 1212
Content-Type: text/html; charset=UTF-8
Connection: Close

// レスポンスボディは省略

この際にはSet-Cookieヘッダは使ったりはしていません。Set-Cookieヘッダを使うのはクッキーを初めてサーバーから送信したい時だけです。

ここまでのクッキーについてのまとめ

  • クッキーとは、(クッキーの名前, クッキーの値)が成すペアと、それに結び付けられた0個以上のメタデータである
  • Set-Cookieヘッダを使うと、サーバーからクライアント(ブラウザ)に対してクッキーを送信できる。クッキーの数だけSet-Cookieヘッダを用意する必要がある。
  • クライアントはサーバーからクッキーが送られてきたら、そのクッキーをおそらくクライアント独自の保存領域に保存する。
  • クッキーに対して特に制限がかけられていなければ、クライアントはクッキー生成元サーバーにリクエストをするたびに、そのサーバーが生成したクッキーをCookieヘッダを使って送信する。(Cookieヘッダにはクッキー名とクッキーのvalueしか指定せず、クッキーのメタデータは指定しないので注意)
  • Cookieヘッダー自体はHTTPリクエストメッセージに1つだけ含めることができる。そのため、複数のクッキーを送る場合は;で区切る

その他のクッキーについて

セッションクッキーとかサードパーティークッキーとか他にもいろんな種類のクッキーはありますが、この記事で説明しているクッキーが理解できれば理解できる内容なので、あえて説明を省きます。詳細な説明については以下のスクラップにまとめています。

zenn.dev

セッションとは

セッションとは、Webサイトを訪れたユーザーがサイト内で行う一連の行動のことです。一連の行動をまとめて1セッションとしてカウントされます。実際のWebアプリ開発ではログインからログアウトまでを1つのセッションとして扱うことが多いです。

セッションを確立することで、前のリクエストの結果を考慮した上で、次のリクエストが出せることです。

例えばログイン後にセッションがちゃんと確立されていれば、別のページをリクエストしてもリロードしてページをリクエストしても、ログイン状態が保持されます。ログイン後にセッションが確立されていない場合、別のページをリクエストしたりリロードした際にログイン状態が保持されません。

セッション管理とは

セッション管理とは、なんらかの仕組みでクライアントのセッションを管理することです。

セッション管理をどのような仕組みで実現するかをざっくりいうと、クッキー + セッションストレージ(セッション情報を保存する場所)によって実現されます。

クッキーはセッションを識別するためのid(セッションid)を保存するのに使います。 セッションストレージには、セッションidとセッションidに紐づくユーザidを保存します。

セッションストレージにはクッキー or Redisを選択することが多いのかなと個人的には思います。それぞれのセッションストレージを選んだ場合の、セッション管理についてまとめます。

セッション管理の方法 セッションストレージ メリット デメリット
クッキーのみ クッキー ストレージを用意する必要がない クッキー自体に容量制限がある。サーバー側でセッションクリアできない(ログアウトする前にクッキーをコピーしておいて、ログアウトした後にそのクッキーをコピーしてリクエストしたらセッションが確立できちゃう。これはセッションリプライ攻撃という)
クッキー + Redis Redis 容量制限がない, サーバー側でセッションをクリアできる ストレージを用意する必要があり、ゆえにコストがかかる。RedisへのI/Oコストが発生する(メモリを圧迫するので、あまり多くの情報を保存しすぎない)。Redisサーバが落ちると認証周りの機能が全て動かなくなるので、可用性のあるインフラ設計をする必要がある。

Railsのデフォルトの設定だと、クッキーのみでセッションを管理しています。

セッションとセッション管理の関係性

セッションはwebサイトを訪れたユーザーがサイト内で行う一連の行動のことなので、セッションとは、概念的なものなのかなと個人的には思います。

セッション管理は、なんらかの仕組みを使ってクライアントのセッションを管理すること(つまり、どのようにセッションを実現するか)を意味する言葉なのかなと個人的には思います。

クッキーとセッションの関係性

クッキーはユーザーの設定を登録したり、ユーザーの行動を記録するマーケティングのために使ったりしますが、セッションを実現するためにも使います(厳密にいうとセッション管理を実現するために使います)。

セッション管理の流れ

以下で、どのような流れでセッション管理がされているかをまとめます。セッション管理の方式は、クッキー + Redisとします。

  1. クライアントがサーバーに対してログインフォームを要求する
  2. サーバーがログインフォームを返す
  3. ログイン情報を入力して、サーバーに送信する
  4. ログイン情報を元にユーザー認証をする(クライアントが誰なのかを特定する)。ユーザーが特定できたら、セッションストレージ(Redis)に生成したセッションidとユーザーidを格納する。そして、session_idを表すクッキーとマイページをクライアントに送信する。
  5. クライアントにマイページが表示される
  6. クライアントはリロードしたり別のページをリクエストすると、クッキーも一緒にサーバーに送信される。サーバー側ではクッキーからsession_idを取得して、そのsession_idでRedisにアクセスしてユーザーidを取得する。そうすることで、サーバーはユーザーの状態を保持したままレスポンスを返せる。
  7. ログアウト時は、クッキーとセッションストレージの情報を消します。ログアウトのリクエストが来た時に、サーバー側では同名のクッキーのExpiresに過去日を指定してクライアントに送信します。そうすれば、クライアントは勝手にクッキーを消してくれます。また、Redisに登録してあるセッション情報も削除しておきます。このセッション情報を削除しないと、セッションidをコピーしてリクエストしたらログイン状態を維持できてしまいます。

  8. 終わり

サインアップ機能, サインイン機能, ログアウト機能が満たす必要がある機能要件

事前にDBにpassword_digestというカラムを作る必要があります。

サインアップ機能

  • クッキーが存在しないことをミドルウェアで確認する
    • クッキーが存在するならマイページへリダイレクト
    • ミドルウェアでは、session_idというクッキーが存在するのかと、そのsession_idでユーザーが実際に存在するかを確認する。もしユーザーが実際に存在するかを確認しないと、デタラメなsession_idでもOKになっちゃう。
  • GET /sign_upでは、サインインページをレスポンスする
  • POST /sign_upでは、emailがユニークであるか(そのemailを持ったユーザーが既に存在するか)、パスワードとパスワードカンファが一致しているかを確認する。OKなら、パスワードダイジェストを生成してユーザーデータをインサートする。その後、セッションストレージにuser_idを格納して、そのuser_idに紐づくsession_idをクッキーに入れる(セッション管理)。その後マイページへリダイレクトさせる(リダイレクトしないと、POST /sign_upのレスポンスとしてページをレスポンスすることになる。パスが /sign_upなのでユーザーからしたらすごい違和感がある)
    • session_idクッキーのvalueにはユニークかつパターンを推測できないものが良いので、uuidとかが良い。
  • もしPOST /sign_upに失敗したら、サインアップフォームをもう一度返す

サインイン機能

  • クッキーが存在しないことをミドルウェアで確認する
    • クッキーが存在するならマイページへリダイレクトさせる
  • GET /loginでは、ログインページをレスポンスする
  • POST /loginでは、emailでユーザーを特定する。emailでユーザーを特定できないなら、ログインフォームを返す。その後、passwordをハッシュ化したものとユーザーが持つpassword_digestを比較して同じなら、セッションストレージにuser_idを格納して、そのuser_idに紐づくsession_idをクッキに入れる(セッション管理)。その後、マイページへリダイレクトさせる。同じじゃないなら、ログインページをレスポンスする
  • もしユーザーがログインしていないのに/mypageをリクエストしてきたら、ミドルウェアを使ってログインページにリダイレクトさせる。
    • ミドルウェアでは、session_idというクッキーが存在するのかと、そのsession_idでユーザーが実際に存在するかを確認する。

ログアウト機能

  • ブラウザはDELETEリクエストできるけど、フォームがGETとPOSTしかできなくて、aタグもGETしかできないので、JavaScriptのfetchを使ってログアウト機能にDELETEリクエストする。
  • もしユーザーがログインしていないのにログアウト機能をリクエストしてきたら、ミドルウェアを使ってログインページにリダイレクトさせる。 (ログインしていないのにログアウト機能を使ってくるユーザーは意味不明)
  • DELETE /logoutでは、クッキーからsession_idを取得して、そのsession_idをもとにRedisのセッション情報をを削除する。もしOKなら、session_idというクッキーのExpiresを過去日にしてボディなしでOKを返す。もし、DELETEリクエストじゃないなら、ボディなしのInternal Sever Errorコードを返す。
  • JavaScriptはレスポンスを受け取ったら、location.hrefで、GET /loginを実行する。

認証とセッション管理の関係性をまとめる

Webアプリケーションにおける認証とは、「クライアント(ブラウザ)が誰なのかを特定すること」です。 (補足ですが、認可は「クライアントに権限があるかを特定すること」です)

ユーザー認証しただけだと、ログイン状態を保持できないです。例えばログイン後にセッションがちゃんと確立されていれば、別のページをリクエストしてもリロードしてページをリクエストしても、ログイン状態が保持されます。ログイン後にセッションが確立されていない場合、別のページをリクエストしたりリロードした際にログイン状態が保持されません。

そのため、ユーザー認証した後は必ずセッション管理もしましょう。そうすると、ログイン状態を保持できます。

終わり

実際にセッション管理を作ったり、クッキーのヘッダを使ってみてようやくクッキー、セッション、セッション管理が理解できたなと思いました。 今回はGoとRailsでユーザー認証とセッション管理の仕組みを作りました。

github.com

github.com

Railsが色々抽象化しているので、初心者がRailsでセッション管理や認証機能を作ると、腑に落ちないことが多いのかなと思いました(自分もそうだった)。Goで作ることで、色々考慮しないといけないので、セッション管理や認証周りの理解力は上がったなと思いました。しかし、Railsの方がサクッとできたので、やっぱRailsってすごいなと思いました。

参考記事

Cookieを扱う|伸び悩んでいる3年目Webエンジニアのための、Python Webアプリケーション自作入門

セッションとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

Rails7 × MySQLの環境をdocker-composeで立ち上げる(ついでにRailsで認証とセッション管理の仕組みも作る)

安全なウェブサイトの作り方 - 1.4 セッション管理の不備 | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構

セッション管理とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

Goでログインとサインアップの認証機能を作る

railsのsessionの値の保存先 - mikami's blog

Railsのセッション管理には何が最適か #Rails - Qiita

RailsでセッションとCookieを操作する方法 | Enjoy IT Life

https://fintan.jp/wp-content/uploads/2021/12/eafb1d61d2382a450ded5d97a4a2c464.pdf

rails-authentication/server/app/controllers/registers_controller.rb at main · yukiHaga/rails-authentication · GitHub