Railsノート - セッションまわりを読む (2) - セッションの保存/復元のロジック

途中で力尽きたというか、こういうリーディングのやり方は効率に問題があると悟った(笑)ので、後半ぐだぐだですが、やった分だけうpします。

セッション → Cookie

セッションが Cookie に保存されるロジックから見た方がわかりやすいと思うのでそっちから。CookieStore.call(env) の後半部分(@app.call(env) の後)を見る。

# action_controller/session/cookie_store.rb

module ActionController
  module Session
    class CookieStore

      # snip

      def call(env)
        env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
        env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup

        status, headers, body = @app.call(env)

        session_data = env[ENV_SESSION_KEY]
        options = env[ENV_SESSION_OPTIONS_KEY]

        if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
          session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
          session_data = marshal(session_data.to_hash)

          raise CookieOverflow if session_data.size > MAX

          cookie = Hash.new
          cookie[:value] = session_data
          unless options[:expire_after].nil?
            cookie[:expires] = Time.now + options[:expire_after]
          end

          cookie = build_cookie(@key, cookie.merge(options))
          unless headers[HTTP_SET_COOKIE].blank?
            headers[HTTP_SET_COOKIE] << "\n#{cookie}"
          else
            headers[HTTP_SET_COOKIE] = cookie
          end
        end

        [status, headers, body]
      end

セッションを Cookie の「値」へと永続化しているのはこの部分↓

session_data = marshal(session_data.to_hash)

marshal の定義は

        # Marshal a session hash into safe cookie data. Include an integrity hash.
        def marshal(session)
          @verifier.generate(persistent_session_id!(session))
        end

persistent_session_id! は新規セッションにセッションID を含める処理。クッキーストアの場合、Cookie そのものがセッションなので紐付けのためのセッションID は本来不要なはずだが、コントローラはそんなこと知らない。

@verifier ってなんなんじゃコラ。

      def initialize(app, options = {})
        # snip
        @secret = options.delete(:secret).freeze

        @digest = options.delete(:digest) || 'SHA1'
        @verifier = verifier_for(@secret, @digest)
        # snip
      end

      # snip

        def verifier_for(secret, digest)
          key = secret.respond_to?(:call) ? secret.call : secret
          ActiveSupport::MessageVerifier.new(key, digest)
        end

セッションを Cookie の「値」へと(改竄/偽造予防の措置をとりつつ)永続化するロジックを持っているのがこの ActiveSupport::MessageVerifier というクラス。

ActiveSupport::MessageVerifier
# activesupport/lib/active_support/message_verifier.rb 

module ActiveSupport
  class MessageVerifier

    def initialize(secret, digest = 'SHA1')
      @secret = secret
      @digest = digest
    end

    # snip

    def generate(value)
      data = ActiveSupport::Base64.encode64s(Marshal.dump(value))
      "#{data}--#{generate_digest(data)}"
    end

    # snip

      def generate_digest(data)
        require 'openssl' unless defined?(OpenSSL)
        OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), @secret, data)
      end
  end
end

この辺は以下のエントリで解説されている通りだ。*1

Cookie → セッション

めんどくさいので省略*2 → まとめ参照

まとめ

ファジーなまとめ。

AbstractStore::SessionHash
 ↓
marshal
 ↓
ActiveSupport::MessageVerifier#generate → generate_digest
 ↓
Cookie
 ↓
unmarshal
ActiveSupport::MessageVerifier#verify → secure_compare
 ↓
AbstractStore::SessionHash

*1:リンク先エントリの投稿日付を見るにつけ、おいおい俺って何周遅れなんだよ、と気が滅入るばかり(笑)

*2:こんな調子でやってたらほとんどすべてのコードを貼り付けなければならなくなる。時間がかかってしょうがないw リーディングのやり方をもうちょっと効率的なものにする必要がある。