Open49

いまだにsessionとかcookieがよくわからん話

kappazkappaz

セッションとかcookieがよくわからんので理解するために何かする。

kappazkappaz

bcrypt...パスワードのハッシュ化に使われるアルゴリズム

Ruby実装
https://github.com/bcrypt-ruby/bcrypt-ruby

ハッシュ化されたパスワードは、暗号化されたパスワードとよく誤解されがちです。例えば、(実は本書の第1版や第2版でも間違っていたのですが) has_secure_passwordのソースコードでもこの手の間違いがあります。というのも、専門用語としての「暗号」というのは、設計上元に戻すことができることを指します (暗号化できるという文には、復号もできるというニュアンスが含まれます)。一方、「パスワードのハッシュ化」では元に戻せない (不可逆) という点が重要になります。したがって、「計算量的に元のパスワードを復元するのは困難である」という点を強調するために、暗号化ではなくハッシュ化という用語を使っています。

ほーなるほど...確かにハッシュ化と暗号化は違うという話をどこかで聞いた気がするがそういうことだったのか。

kappazkappaz

has_secure_password

https://github.com/rails/rails/blob/main/activemodel/lib/active_model/secure_password.rb

内部的には InstanceMethodsOnActivationxxx =メソッドにおいて、 xxx_digit に bcyrptでハッシュ化したパスワードを入れるようにしているっぽい

        define_method("#{attribute}=") do |unencrypted_password|
          if unencrypted_password.nil?
            instance_variable_set("@#{attribute}", nil)
            self.public_send("#{attribute}_digest=", nil)
          elsif !unencrypted_password.empty?
            instance_variable_set("@#{attribute}", unencrypted_password)
            cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
            self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
          end
        end

つまりチュートリアルの以下かな

セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる

ちょっと納得

kappazkappaz

全然進んでないが一旦区切り。
has_secure_passwordの中身を見れたことが少し収穫

kappazkappaz

ActiveModel::ForbiddenAttributesError in UsersController#create
Strong Parameter使ってないとエラー出るのか。出るんだっけ?なんか知らなかった。
でもRails5のドキュメントの段階から書いてあるならただ知らなかっただけだな。

kappazkappaz

metal.rbはActionController固有の基本クラスっぽい

kappazkappaz

まぁとにかく

      flash[:success] = "Welcome to the Sample App!"

とやると、

class Flash
  class FlashHash
    def []=(k, v)
      k = k.to_s
      @discard.delete k
      @flashes[k] = v
    end
  end
end

ここで@flashesに入るわけだ。

でもリクエスト跨いだらflashesの中身消えませんか?
あーなんかコミットってあったなそういえば。

kappazkappaz

Flash::RequestMethod#commit_flash

          session["flash"] = flash_hash.to_session_value

的な感じでsessionにflashの内容を登録しているのか。

いい感じに sessionってなんですか?というところにたどり着けたわ。
(railsにおけるsession = ユーザー毎の情報を格納するための仕組み。雰囲気で使ってたな...)

それは良いとして、どこでクリアしてるんだ?多分discard周りだと思うけど。

kappazkappaz

あーdiscardはfrom_session_valueのnewで設定されるのか


      def self.from_session_value(value) # :nodoc:
        case value
        when FlashHash # Rails 3.1, 3.2
          ...
        when Hash # Rails 4.0
          flashes = value["flashes"]
          if discard = value["discard"]
            flashes.except!(*discard)
          end
          new(flashes, flashes.keys)
        else
          new
        end
      end
      ...
      def initialize(flashes = {}, discard = []) # :nodoc:
        @discard = Set.new(stringify_array(discard))
        @flashes = flashes.stringify_keys
        @now     = nil
      end

でそのリクエストのcommit_flashで session["flash"] = flash_hash.to_session_value によってsession内のflashが削除されるのかなるほど。

      def commit_flash # :nodoc:
        ...
        if flash_hash && (flash_hash.present? || session.key?("flash"))
          session["flash"] = flash_hash.to_session_value
          self.flash = flash_hash.dup
        end
      end

      def to_session_value # :nodoc:
        flashes_to_keep = @flashes.except(*@discard)
        return nil if flashes_to_keep.empty?
        { "discard" => [], "flashes" => flashes_to_keep }
      end

ずっと見落としてたんですけどかなしい

kappazkappaz

そうだ元々Railsチュートリアルの流れにのってたんだった。
戻ろうか、もう少しsessionを深堀しようか...
戻ろう

kappazkappaz

ここでいうログインの基本的な仕組みとは、ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄するといった仕組み (認証システム (Authentification System))です。

sessionとかcookieとかってよく使われるのはここですよね。
なんかどうしてもネットワークのセッションが頭をよぎるんだ...

kappazkappaz

こういうことするにはzennのスクラップが一番良いな

kappazkappaz

ユーザーログインの必要なWebアプリケーションでは、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に別途設定します。セッションはHTTPプロトコルと階層が異なる (上の階層にある) ので、HTTPの特性とは別に (若干影響は受けるものの) 接続を確保できます。

概念的にはこれが全てか。WebアプリケーションにおけるセッションというのはステートレスなHTTP上でユーザー識別等のステートを保持しておくための仕組みという話。

Railsにおけるsessionはその実現のための仕組みと。cookieはその実現のためによく使われる手段の1つ的な位置付けかな...やっぱり同階層の話じゃないよね。

kappazkappaz

Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。cookiesは、あるページから別のページに移動した時にも破棄されないので、ここにユーザーIDなどの情報を保存できます。

まぁそうですよね。1度やったけど当時は理解の解像度が低かった的な感じなんかな。なんかこの辺ぼんやりとしてたな...

セッションをRESTfulなリソースとしてモデリングできると、他のRESTfulリソースと統一的に理解できて便利です

sessions_controllerってまぁよくあるけどなんも考えてなかったわ。sessionをRESTfulなリソースとして扱ってるってことか

ログインページではnewで新しいセッションを出力し、そのページでログインするとcreateでセッションを実際に作成して保存し、ログアウトするとdestroyでセッションを破棄する、といった具合です。

これセッションについての具体的なイメージがなかったから頭に入っていなかったな。いや今も明確には理解してないぞ。

kappazkappaz

一旦 リスト 8.4:ログインフォームのコード まで。
railsのform周り(form_withとか)のこともあんまり理解してない。
というかform系全般理解があやふや

kappazkappaz

なんかflash周りの表示、turbo-railsを取り除かないとうまく表示できないっぽい。なんかあったなこんなの...

kappazkappaz

sessionメソッドで作成した一時cookiesは自動的に暗号化され、リスト 8.14のコードは保護されます。そしてここが重要なのですが、攻撃者がたとえこの情報をcookiesから盗み出すことができたとしても、それを使って本物のユーザーとしてログインすることはできないのです。ただし今述べたことは、sessionメソッドで作成した「一時セッション」にしか該当しません。cookiesメソッドで作成した「永続的セッション」ではそこまで断言はできません。永続的なcookiesには、セッションハイジャックという攻撃を受ける可能性が常につきまといます。ユーザーのブラウザ上に保存される情報については、第9章でもう少し注意深く扱うことにします。

この辺はシンプルに分からんが、どちらも(sessionは初期設定通り)cookieを使っている想定ならcookieの設定で実現してるとかだろうか?

kappazkappaz

ちょっと整理するか。

Session

Webアプリケーションにおいて、ステートレスなHTTP上で主にユーザーに関する状態を保持するための仕組み。ユーザーがログインしているかどうかや、ユーザーの一時的なデータを保持するために使用される。

Railsでは、この目的のためにsessionメソッドが提供されている。sessionのデフォルトの実装はcookieを使用するが、設定に応じて他のストレージに変更することも可能。

kappazkappaz
  • CS分野におけるセッションは「継続的な通信」もしくはそれを実現するための仕組み、のこと
  • 継続的な通信にはそれを管理するためのなんらかの状態(ステート)が必要
  • ステートフルなプロトコルを用いた通信では、プロトコル自体に継続的な通信を確立するための仕組みが入っている
  • ステートレスなプロトコルには継続的な通信を確立するための仕組みは無いので、何かしらの仕組みを追加で導入する必要がある。
    • 往々にしてこの仕組み自体もセッションと呼ばれる
    • cookieはステートレスなhttpにセッションを導入するための主要な手段の1つ

まぁまぁしっくりくる表現。ひとまずこれを土台にして良いかな。

kappazkappaz

ChatGPTって、しっくりくる表現を探すための手段として便利だな...
馬鹿になってる気もするけど、今までググったり本読んだりで探していたものを代替していると思えば実態としては変わらないかもしれない。
結局のところ知ってる、扱える範囲の言葉しか拾えないだろうし。

kappazkappaz

じゃあ改めてこれ
https://railsguides.jp/action_controller_overview.html#セッション

Railsアプリケーションは、ユーザーごとにセッションを設定します。

この場合のセッションは

  • セッションを保持するための仕組み
  • セッションストア(上に包含されるか?)

という感じだろうか。
言い換えれば継続的な通信を確立するための仕組みをユーザ毎に設定するという感じ?いや...継続的な通信を確立するための仕組みがもともとあって、それをユーザー毎にセットアップするぜという感じ?

セッション(という仕組みを使って)ユーザーを識別できるようにします
の方があってるかな...?
セッション(という仕組みを使って) ユーザー毎に状態をもたせられるようにします、でも良い気もするが...
総称して「セッションを設定します」なのかね。

前のリクエストの情報を次のリクエストでも利用するために

これは前のリクエストと次のリクエストを継続的な通信として扱う、
ということと大体同じ意味になるかな。

kappazkappaz

Railsアプリケーションは、ユーザーごとにセッションを設定します。

でもやっぱこれわかんねぇな...イメージできない。何してんの?
コードみるか...

kappazkappaz

@byはなんですか?と。

https://github.com/rails/rails/blob/34ab9b9270d5ab1019540b88edb2a644130fae88/actionpack/lib/action_dispatch/request/session.rb#L76-L77
https://github.com/rails/rails/blob/34ab9b9270d5ab1019540b88edb2a644130fae88/actionpack/lib/action_dispatch/request/session.rb#L19-L27

よりActionDispatch::Request::Session.create で設定されている様子。

このcreateはActionDispatch::Session::SessionObject#prepare_session
で呼び出されている

https://github.com/rails/rails/blob/34ab9b9270d5ab1019540b88edb2a644130fae88/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb#L77-L79

selfを見てみると...

(rdbg) list 75    # command
    75|       def prepare_session(req)
=>  76|         debugger
    77|         Request::Session.create(self, req, @default_options)
    78|       end
    79| 
    80|       def loaded_session?(session)
    81|         !session.is_a?(Request::Session) || session.loaded?
    82|       end
    83|     end
    84| 
(ruby) self.class
ActionDispatch::Session::CookieStore

なので改めて

https://github.com/rails/rails/blob/34ab9b9270d5ab1019540b88edb2a644130fae88/actionpack/lib/action_dispatch/request/session.rb#L273

は、
ActionDispatch::Session::CookieStore の load_sessionを呼び出していることになる。

kappazkappaz

unpacked_cookie_dataget_cookie(req) で cookieを読み出して、headerにセットしている

https://github.com/rails/rails/blob/34ab9b9270d5ab1019540b88edb2a644130fae88/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb#L120-L126

request.cookie_jar.signed_or_encrypted では基本暗号化されたcookie_jar(cookieの入れ物的な概念)を返す。

cookie_jar(req) には EncryptedKeyRotatingCookieJar のインスタンスが入っており、
[@key] は以下の形で parseされた(おそらく複合された) cookieの中身が参照できる

https://github.com/rails/rails/blob/34ab9b9270d5ab1019540b88edb2a644130fae88/actionpack/lib/action_dispatch/middleware/cookies.rb#L511-L513

https://github.com/rails/rails/blob/34ab9b9270d5ab1019540b88edb2a644130fae88/actionpack/lib/action_dispatch/middleware/cookies.rb#L683-L689

kappazkappaz

ということで参照周りはなんとなくイメージがついた。

kappazkappaz

callerで見てみる

(ruby) puts caller
.../gems/debug-1.9.2/lib/debug/thread_client.rb:415:in `eval'
(中略)
.../gems/debug-1.9.2/lib/debug/session.rb:2641:in `debugger'
.../gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/session/abstract_store.rb:76:in `prepare_session'
.../gems/rack-session-2.0.0/lib/rack/session/abstract/id.rb:271:in `context'
.../gems/rack-session-2.0.0/lib/rack/session/abstract/id.rb:266:in `call'
.../gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/cookies.rb:689:in `call'
(中略)
.../gems/3.3.0/gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/callbacks.rb:28:in `call'
.../gems/3.3.0/gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/executor.rb:14:in `call'
(中略)
.../gems/3.3.0/gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/request_id.rb:28:in `call'
(中略)
.../gems/3.3.0/gems/puma-6.4.2/lib/puma/request.rb:99:in `handle_request'
(中略)
.../gems/3.3.0/gems/puma-6.4.2/lib/puma/thread_pool.rb:155:in `block in spawn_thread'

細かいことは分からんが、
リクエストがあったタイミングで、rack-session-2.0.0/lib/rack/session/abstract/id.rb:271 で呼び出されている様子。

まぁそもそも middleware/session/abstract_store.rb 自体middlewareと言ってるんだし、rackの領域の話なんだろうな。

ということで次は rack-session

kappazkappaz
.../gems/rack-session-2.0.0/lib/rack/session/abstract/id.rb:271:in `context'

の該当箇所は以下。

https://github.com/rack/rack-session/blob/f9b045c20eddb940040500a7351d584372ce760d/lib/rack/session/abstract/id.rb#L271-L277

が、すぐ下に prepare_session が定義されている

https://github.com/rack/rack-session/blob/f9b045c20eddb940040500a7351d584372ce760d/lib/rack/session/abstract/id.rb#L309-L314

stack_traceは

.../gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/session/abstract_store.rb:76:in `prepare_session'

って言ってるんで継承が関連してくるんだろう。が、こいつは Persisted クラスのインスタンスメソッドなので include されてる系ではないな...

kappazkappaz

https://chatgpt.com/share/622ede8f-9c04-4e0c-9277-8e8166d438e0

このコメントが説明しているのは、Rack::Session::Abstract::IDというモジュールやクラスが提供する「IDベースのセッション管理フレームワーク」についてです。このフレームワークは、セッション管理をIDを使って行うための基本的な枠組みを提供します。

具体的には、セッションを維持するために、クライアントに送られるクッキーにはセッションデータ自体ではなく、セッションを識別するための「ID」が保存されます。そして、サーバー側ではそのIDをもとに、対応するセッションデータを管理します。

ほーんなるほど...?

https://railsguides.jp/action_controller_overview.html#セッション

これかな。

あらゆるセッションは、cookieを利用してセッション固有のIDを保存します(cookieは必ず使うこと: セッションIDをURLで渡すとセキュリティが低下するため、この方法はRailsで許可されません)。

ほとんどのセッションストアでは、サーバー上のセッションデータ(データベーステーブルなど)を検索するときにこのIDを使います。

CookieStoreは、Railsで推奨されているデフォルトのセッションストアであり、例外的にすべてのセッションデータをcookie自身に保存します(必要に応じてセッションIDも利用可能です)。CookieStoreには非常に軽量であるというメリットがあり、新規Webアプリケーションでセッションを利用するための準備も不要です。このcookieデータは改ざん防止のために暗号署名が追加されており、cookie自身も暗号化されているので、他人が読むことはできません(改ざんされたcookieはRailsに拒否されます)。

Rack::Session::Abstract::ID は IDベースといいつつ、デフォルトのCookieStoreを使った場合には 全てのセッションデータを cookie自身に保存するのか。
確かに cookieをdecryptしたやつは中にuser_idとかも入ってるもんな...

ベースの部分ではより汎用的な Rack::Session::Abstract::ID を提供するけど、デフォルトでは CookieStore使ってねという感じか。CoCを感じますなぁ...

kappazkappaz

stack_traceは

.../gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/session/abstract_store.rb:76:in `prepare_session'

って言ってるんで継承が関連してくるんだろう。が、こいつは Persisted クラスのインスタンスメソッドなので include されてる系ではないな...

これの続き。

https://github.com/rails/rails/blob/34ab9b9270d5ab1019540b88edb2a644130fae88/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb#L44-L52

https://github.com/rails/rails/blob/34ab9b9270d5ab1019540b88edb2a644130fae88/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb#L97

まぁこの通りか。
ActionDispatch::Session::CookieStoreRack::Session::Abstract::PersistedSecure を継承していると。だからおそらく ActionDispatch::Session::CookieStore が callされて、 context メソッド内で prepare_sessionが呼ばれた際に、lib/action_dispatch/middleware/session/cookie_store.rb の prepare_sessionが呼ばれるということだろうな。

kappazkappaz

整理すると...

前提

  • CS分野でのセッション = 「継続的な通信 及びそれを実現するための仕組み」
  • Web分野でのセッション = 「ステートレスなhttp上での継続的な通信 及びそれを実現するための仕組み」
    • 基本的なユースケースとしては「ユーザーの状態を維持すること」が挙げられる。(ログイン状態を維持したり、ショッピングサイトにおけるカートの中身を維持したりなど)

Ruby on Railsにおけるセッション

概要

  • Railsアプリケーションは、ユーザーごとにセッションを設定する
    • Railsガイドより
    • 「ユーザーごとにセッションを設定する」とは...
      • サーバーリクエストに対してセッションIDを発行する
      • そのセッションID毎にユーザーを識別する

実装

おおよそ以下3パートに分かれる

  • セッションの初期設定
    • セッションストアの選択。デフォルトでは CookieStore
    • Rails.application.config.session_store :cookie_store 的なやつ
  • サーバーがリクエストを受け取った際、Rack Middleware の階層で req(多分ActionDispatch::Request のインスタンス)のheaderに、ActionDispatch::Request::Sessionのインスタンスを設定する。その中には前項で設定したセッションストア も格納されている
    • ちょっと怪しい...
  • アプリがsessionを参照した場合
    • ここで初めてセッションがloadされる(@by.load_session @req)
    • CookieStoreの場合はこの段階でリクエストヘッダーからcookieを読みに行く形(多分)
kappazkappaz

改めて整理

1. Railsアプリ立ち上げ時、利用するSessionStoreを設定

  • Rails.application.config.session_store :cache_store こういうやつ
  • おそらくこの設定によって2.が起きるようになると思われる

2. リクエスト受け取り時、Sessionインスタンスを作成

  • 端的には
    • RackでActionDispatch::Session::CookieStore#callによって、セッション管理用のインスタンスが作成されている
  • 詳細としては...
    • Rack::Session::Abstract::Persisted#context
      • 実体としては ActionDispatch::Session::CookieStoreCookieStorePersistedを継承している
    • ActionDispatch::Session::SessionObject#prepare_sessionActionDispatch::Requestインスタンスの@envに、ActionDispatch::Session インスタンス( ActionDispatch::Session::CookieStoreのインスタンスを内包する)を設定する。
      • その際のkeyはRack::RACK_SESSION

3. session 参照時、2.で作成したsessionインスタンスからセッションデータをロード

  • 端的には...
      1. で登録した session内の session store の load_session メソッドを用いて、sessionデータを読み出す
  • 詳細は...
    • controller内で呼ばれるsessionメソッド
    • ActionDispatch::Metaldelegate :session, to: "@_request" -> ActionDispatch::Requestinclude Rack::Request::Helpers#session
      • ActionDispatch::Request::Session を取得する
    • session[:key]ActionDispatch::Request::Session#[]で実装される
    • load_for_read! -> load!@by.load_session @reqでセッションがリードされる。@byの中には2.で設定した SessionStoreのインスタンスが入っている。デフォルトではCookieなので、ActionDispatch::Session::CookieStoreのインスタンスになる
    • ActionDispatch::Session::CookieStore#load_sessionで、cookieの中身を複合化して session_id と セッションデータのセットを返す
kappazkappaz

Rackとかの説明を以下に書いたが、執筆中の記事で使わなくなった。勿体無いのでここに残す。

関連機能(クラス/モジュール)

今回参照する主な関連機能(クラス/モジュール)は以下の通り

  • ActionDispatch
    • ActionDispatch::Request
    • ActionDispatch::Session
    • ActionDispatch::Middleware::Session::CookieStore
  • Rack
    • Rack::Request
  • Rack::Session
    • Rack::Session::Abstract::SessionHash
    • Rack::Session::Abstract::Persisted

補足説明

上に挙げたActionDispatch/Rack/Rack::Sessionについて本記事に関連する範囲の概要を記載する。
これらに関してイメージがつく方はこの項目は読み飛ばしてもらって構わない。

ActionDispatch

ActionDispatchは、Railsのリクエスト処理に関する機能群(クラス/モジュール)であり、セッション管理やエラーハンドリング、ルーティングなどを行うクラスを含んでいる。
これらはRailsのミドルウェアスタックで動作するものもあれば、アプリケーション上で使用される機能やデータ(例えばルーティング機能自体やコントローラ内で参照可能なリクエストに関するデータ)を提供するものもある。
この記事では主にセッションに関連する、ミドルウェア、データや機能について触れる。

Rack

RackはWebサーバーとアプリケーションの間のインタフェースを提供し、HTTPリクエスト処理を抽象化している。Rails等のWebフレームワークは、Rackを用いることでWebサーバーからのHTTPリクエストを標準化された方法で受け取り、処理できるようになる。
この記事では主に、Rackの環境変数の情報を操作・取得するためのヘルパーメソッド群について触れる。

Rack::Session

Rack::SessionはRackアプリケーション(この場合はRailsアプリケーション)にセッション機能を追加するためのミドルウェアで、セッション管理のための基本的な機能を提供する。Railsが定義するセッションストアはここで提供されているものをベースにして作成される。
この記事では主に、リクエストを受け取った際にセッションが用意される処理の流れ、およびセッションが参照される際の処理の流れについて触れる。

kappazkappaz

まだこの記事を書いてるわけだが...
https://zenn.dev/articles/cefd6d7bf338d6/edit

補足として以下は加えたい

  • source_locationとかbacktraceとかは使っている
  • rubyでxxx[:key] とかは、xxxの[]メソッド&引数(key)で表される(rubyの公式ドキュメント)
kappazkappaz

この話1ヶ月くらいやってんな。
記事にすると倍くらいかかるという話。
まぁ趣味だから...と心の底から思えるようになりたいね。ちょっとコスパを気にしている節がある。