Railsノート - セッションまわりを読む (1) - セッションの保存/復元のタイミング

Rails のセッションまわりについて調べる。知りたいのは (1) Cookie に含まれている情報からセッション(sessionメソッドの返値となるオブジェクト)を作るところと、その逆に、(2) セッションを Cookie としてレスポンスに含めるところだ。イメージとしては、

セッション ⇔ Cookie ⇔ リクエスト/レスポンス

というような関係にあると思うのだが、その辺を確認したい。セッションストアは Cookie Store とする。例のごとく、参照する Rails のコードは 2.3.5

Cookie

Cookie としてクライアントに送信されるセッションは以下のようになる。(sandbox はアプリケーションの名前)

_sandbox_session=BAh7BjoPc2Vzc2lvbl9pZCIlN2UwZTA5MTQ2NWVjY2Q4NjYxMjBlMjI4YWEzZWMxZDg%3D--8bfad135e5bb257c95eb5438625d6d058b5b9636; path=/; domain=localhost; HttpOnly

この Cookie の中身に関しては以下のエントリが非常に参考になる。感謝。

今回確認したいのはクライアントから送られてきた上記のような Cookie からセッションが復元されるところと、その逆に、セッションが Cookie に保存されるところだ。

では、ActionController::Base の sessionメソッドの定義から、順番に見ていこう。

ActionController::Base

# actionpack/lib/action_controller/base.rb

    # Holds a hash of objects in the session. Accessed like <tt>session[:person]</tt> to get the object tied to the "person"
    # key. The session will hold any type of object as values, but the key should be a string or symbol.
    attr_internal :session

attr_internal については前に調べた。セッションは @_session というインスタンス変数に入る。で、@_session はコントローラの initialize に相当する assign_shortcuts において以下のように定義される。

      def assign_shortcuts(request, response)
        @_request, @_params = request, request.parameters

        @_response         = response
        @_response.session = request.session

        @_session = @_response.session
        @template = @_response.template

        @_headers = @_response.headers
      end

@_session = @_response.session = request.session

つまり、request.session と @_response.session は同一のオブジェクトを指しており、コントローラの sessionメソッドはそのショートカットである。

ActionController::Request

module ActionController
  class Request < Rack::Request

    # snip

    def session
      @env['rack.session'] ||= {}
    end

    def session=(session) #:nodoc:
      @env['rack.session'] = session
    end

    def reset_session
      @env['rack.session.options'].delete(:id)
      @env['rack.session'] = {}
    end

    def session_options
      @env['rack.session.options'] ||= {}
    end

    def session_options=(options)
      @env['rack.session.options'] = options
    end

CookieJar と違い、セッションはただのハッシュのようだ。

ここにはクライアントから送られてきた Cookie からセッションストアの種類に応じたやり方でセッションを復元(deserialize)するコードはない。それを探す。actionpack/lib/action_controller.rb にて autoload されているモジュールの中にそれがあるはずだ。まず、コントローラは使用するセッションストアの種類を知る必要がある。

ActionController::SessionManagement

# actionpack/lib/action_controller/session_management.rb

module ActionController #:nodoc:
  module SessionManagement #:nodoc:
    def self.included(base)
      base.class_eval do
        extend ClassMethods
      end
    end
    
    module ClassMethods
      # Set the session store to be used for keeping the session data between requests.
      # By default, sessions are stored in browser cookies (<tt>:cookie_store</tt>),
      # but you can also specify one of the other included stores (<tt>:active_record_store</tt>,
      # <tt>:mem_cache_store</tt>, or your own custom class.
      def session_store=(store)
        if store == :active_record_store
          self.session_store = ActiveRecord::SessionStore
        else
          @@session_store = store.is_a?(Symbol) ?
            Session.const_get(store.to_s.camelize) :
            store     
        end
      end
        
      # Returns the session store class currently used.
      def session_store
        if defined? @@session_store
          @@session_store
        else
          Session::CookieStore
        end
      end

session_store が返すのはセッションの保存/復元ロジックを持ったクラス。これを別のものに置き換えることでコントローラ側はコード修正なしに複数のセッションストアに対応できる。いわゆる Strategyパターン。

では、この session_store が返すクラスを使ってセッションの保存/復元を行っている箇所はどこだろう。

middlewares.rb

# actionpack/lib/action_controller/middlewares.rb

use "Rack::Lock", :if => lambda {
  !ActionController::Base.allow_concurrency
}

use "ActionController::Failsafe"

use lambda { ActionController::Base.session_store },
    lambda { ActionController::Base.session_options }

use "ActionController::ParamsParser"
use "Rack::MethodOverride"
use "Rack::Head"

use "ActionController::StringCoercion"

なんと、セッションの復元/保存はミドルウェアの層で処理されていた。ということは、セッションストアクラス(ここでは CookieStore)は Rack application object ということになる。call(env) にセッションの保存と復元のコードがあるはず。

ActionController::Session::CookieStore

# 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

ここだな。

  1. Cookie からセッションを復元(※実際には遅延ロード → 補足)
  2. アプリケーション実行
  3. セッションを Cookie に保存

と非常にシンプルだ。

セッションの保存/復元(※実際には遅延ロード → 補足)ミドルウェアの層で行われることがわかったので、次は保存と復元のロジックを個別に見ていこうと思う。

補足:SessionHash は Cookie からのセッションのロード(復元)をハッシュの要素が実際に参照されるまで遅延するので、実際の復元タイミングはもう少し後になる。コントローラがセッションを一切使わなければ、当然復元もされない。上記のコードにおいて、セッションを Cookie に保存するにあたり、session_data.send(:loaded?) でセッションが実際にロードされたかどうかを確かめているのもそのため。