Railsのsessionがどうやって設定されるのかをコードリーディングをして追ってみました。Railsのバージョンは5.1.4です。
今回はセッションストアにRedis(redis-store)を使ったケースを追ってみます。
ActionController::Metal#sessionはActionDispatch::Request#sessionにdelegateし、Rack::Request::Env#session(Rack::Request::HelpersやRack::Request::Envなどをinclude)を呼び出します。
module Rack
class Request
module Env
def fetch_header(name, &block)
@env.fetch(name, &block)
end
module Helpers
def session
fetch_header(RACK_SESSION) do |k|
set_header RACK_SESSION, default_session
end
end
#sessionはfetch_header経由で@envのHashから値を取得します。キーはRACK_SESSION = ‘rack.session’です。
env[RACK_SESSION]の値はミドルウェア経由でセットされます。Railsで差し込まれるミドルウェアはRails::Application::DefaultMiddlewareStackで確認できます。
module Rails
class Application
class DefaultMiddlewareStack
def build_stack
ActionDispatch::MiddlewareStack.new.tap do |middleware|
# ...
if !config.api_only && config.session_store
if config.force_ssl && config.ssl_options.fetch(:secure_cookies, true) && !config.session_options.key?(:secure)
config.session_options[:secure] = true
end
middleware.use config.session_store, config.session_options
middleware.use ::ActionDispatch::Flash
end
このconfig.session_storeがSessionのハンドリングになります。 config.session_storeに:redis_store
を指定するとActionDispatch::Session.const_get経由で、ActionDispatch::Session::RedisStoreがミドルウェアとして差し込まれます。
module Rails
class Application
class Configuration < ::Rails::Engine::Configuration
def session_store(new_session_store = nil, **options)
if new_session_store
# ...
@session_store = new_session_store
@session_options = options || {}
else
case @session_store
when :disabled
nil
when :active_record_store
ActionDispatch::Session::ActiveRecordStore
when Symbol
ActionDispatch::Session.const_get(@session_store.to_s.camelize)
else
@session_store
end
end
end
ActionDispatch::Session::RedisStoreは以下のような継承をしています。
module ActionDispatch
module Session
class RedisStore < Rack::Session::Redis
include Compatibility
include StaleSessionCheck
include SessionObject
# ...
moduleを含まない継承ツリーはRack::Session::Redis < Rack::Session::Abstract::ID < Rack::Session::Abstract::Persisted となっており、#callはRack::Session::Abstract::Persisted#callを呼び出します。
module Rack
module Session
module Abstract
class Persisted
def call(env)
context(env)
end
def context(env, app=@app)
req = make_request env
prepare_session(req)
status, headers, body = app.call(req.env)
res = Rack::Response::Raw.new status, headers
commit_session(req, res)
[status, headers, body]
end
#callは#contextを呼び出します。#make_requestはenvを引数にRack::Requestのインスタンスを生成します
ActionDispatch::Session::SessionObject#prepare_sessionはRequest::Session.createを呼び出します。
module ActionDispatch
module Session
module SessionObject # :nodoc:
def prepare_session(req)
Request::Session.create(self, req, @default_options)
end
Request::Session.createはActionDispatch::Request::Sessionのインスタンスを生成し、setメソッドでENV_SESSION_KEY = Rack::RACK_SESSION = ‘rack.session’のキーでenvに設定します。store変数にはActionDispatch::Session::RedisStoreミドルウェアのインスタンス自身が入ります。
module ActionDispatch
class Request
class Session # :nodoc:
def self.create(store, req, default_options)
session_was = find req
session = Request::Session.new(store, req)
session.merge! session_was if session_was
set(req, session)
Options.set(req, Request::Session::Options.new(store, default_options))
session
end
def self.set(req, session)
req.set_header ENV_SESSION_KEY, session
end
コントローラ側でsessionが呼び出されるとenv[‘rack.session’]の値が取り出されるのでActionDispatch::Request::Sessionのインスタンスを取得します。このインスタンス自身は遅延ロードする仕組みなので、[]
などのアクセサを使った場合にロードする処理が走ります。
module ActionDispatch
class Request
class Session # :nodoc:
def [](key)
load_for_read!
@delegate[key.to_s]
end
private
def load_for_read!
load! if !loaded? && exists?
end
def load!
id, session = @by.load_session @req
options[:id] = id
@delegate.replace(stringify_keys(session))
@loaded = true
end
ActionDispatch::Request::Session#load!はActionDispatch::Session::RedisStore#load_sessionを呼び出し、@delegateのHashにセットします。
#load_sessionはStaleSessionCheck#load_session経由でRack::Session::Abstract::Persisted#load_sessionを呼び出します。
module Rack
module Session
module Abstract
class Persisted
def load_session(req)
sid = current_session_id(req)
sid, session = find_session(req, sid)
[sid, session || {}]
end
# Extract session id from request object.
def extract_session_id(request)
sid = request.cookies[@key]
sid ||= request.params[@key] unless @cookie_only
sid
end
# Returns the current session id from the SessionHash.
def current_session_id(req)
req.get_header(RACK_SESSION).id
end
class ID < Persisted
def find_session(req, sid)
get_session req.env, sid
end
#current_session_idはActionDispatch::Request::Session#id経由でRack::Session::Abstract::Persisted#extract_session_idを呼び出すので結果としてセッションキーのクッキー値を取得します。このセッションキーを使ってRack::Session::Redis#get_sessionを呼び出します。
def get_session(env, sid)
if env['rack.session.options'][:skip]
[generate_sid, {}]
else
with_lock(env, [nil, {}]) do
unless sid and session = with { |c| c.get(sid) }
session = {}
sid = generate_unique_sid(session)
end
[sid, session]
end
end
end
Redis::Store#getを呼び出してRedisからセッションキーをキーとしてセッション情報を取得しています。
sessionの設定はコントローラの処理が終わってから、ミドルウェアのapp.call以後にActionDispatch::Session::Abstract::Persisted#commit_sessionによって処理されます。
module Actiondispatch
module Session
module Abstract
class Persisted
def commit_session(req, res)
session = req.get_header RACK_SESSION
# ...
session.send(:load!) unless loaded_session?(session)
session_id ||= session.id
session_data = session.to_hash.delete_if { |k,v| v.nil? }
if not data = write_session(req, session_id, session_data, options)
req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
elsif options[:defer] and not options[:renew]
req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
else
cookie = Hash.new
cookie[:value] = data
cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
set_cookie(req, res, cookie.merge!(options))
end
end
# ...
module ActionDispatch
module Session
class RedisStore < Rack::Session::Redis
def set_cookie(env, session_id, cookie)
if env.is_a? ActionDispatch::Request
request = env
else
request = ActionDispatch::Request.new(env)
end
request.cookie_jar[key] = cookie.merge(cookie_options)
end
def cookie_options
@default_options.slice(:httponly, :secure)
end
#commit_sessionはRedisにセッションを書き込みつつ、Set-Cookieヘッダを書き出す処理を行います。
このように、ActionDispatch::Request::Sessionのインスタンスが格納されるenv[‘rack.session’]
をインターフェースにしてコントローラでセッションの入出力を行い、Rackミドルウェアレベルでセッションのハンドリングをしていることになります。