こんにちは!kossyです!
今回は、deviseのdestroyアクションを実行すると何が起こるか調べてみたので、備忘録としてブログに残してみたいと思います。
環境
Ruby 3.0.3
Rails 6.0.4
devise 4.8.1
前準備
pry-railsとpry-byebugをGemfileに記載してbundleした後、
sessions_controller.rbのdestroyアクションにbinding.pryを仕込みます。
25: def destroy 26: binding.pry 27: signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)) 28: set_flash_message! :notice, :signed_out if signed_out 29: yield if block_given? 30: respond_to_on_destroy 31: end
その後、適当なアカウントでログインして、ログアウトボタンを押してREPLを起動できたらOKです。
Devise.sign_out_all_scopes
From: /app/app/controllers/devise/sessions_controller.rb:27 Devise::SessionsController#destroy: 25: def destroy 26: binding.pry => 27: signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)) 28: set_flash_message! :notice, :signed_out if signed_out 29: yield if block_given? 30: respond_to_on_destroy 31: end > Devise.sign_out_all_scopes => true
Devise.sign_out_all_scopesはconfigファイルで設定できる値かと思われるので、見に行ってみます。
# Set this configuration to false if you want /users/sign_out to sign out # only the current scope. By default, Devise signs out all scopes. # config.sign_out_all_scopes = true
もし users/sign_out でサインアウトさせたい場合は false にする必要があるようです。
デフォルト値は true のようです。
「Deviseはすべてのスコープをサインアウトします。」と記載があるため、
例えば管理者権限と一般ユーザー権限の2つのモデルを作成し、両方の権限でログインしてからログアウトすると、全ての権限でログアウトになると思われます。
sign_out
def sign_out(resource_or_scope = nil) return sign_out_all_scopes unless resource_or_scope scope = Devise::Mapping.find_scope!(resource_or_scope) user = warden.user(scope: scope, run_callbacks: false) # If there is no user warden.logout(scope) warden.clear_strategies_cache!(scope: scope) instance_variable_set(:"@current_#{scope}", nil) !!user end
こちらはREPLで試しながら動作を確認してみます。
$ resource_or_scope
=> nil
このまま1行ずつ実行すると sign_out_all_scopes が実行されてしまうので、resource_or_scopeに値を入れた上で試してみます。
$ resource_or_scope = User.first $ scope = Devise::Mapping.find_scope!(resource_or_scope) => :user $ user = warden.user(scope: scope, run_callbacks: false) => #<User id: 1, ...> $ warden.logout(scope) => nil $ warden.clear_strategies_cache!(scope: scope) => {} $ instance_variable_set(:"@current_#{scope}", nil) => nil $ !!user => true
実際にsessionを削除するような処理はwardenの中で行っているようなので、wardenの処理にstepメソッドで入ってみたいと思います。
From: /usr/local/bundle/gems/warden-1.2.9/lib/warden/proxy.rb:267 Warden::Proxy#logout: 266: def logout(*scopes) => 267: if scopes.empty? 268: scopes = @users.keys 269: reset_session = true 270: end 271: 272: scopes.each do |scope| 273: user = @users.delete(scope) 274: manager._run_callbacks(:before_logout, user, self, :scope => scope) 275: 276: raw_session.delete("warden.user.#{scope}.session") unless raw_session.nil? 277: session_serializer.delete(scope, user) 278: end 279: 280: reset_session! if reset_session 281: end > scopes.empty? => false > user = @users.delete(scope) => #<User id: 1, ...> > raw_session => #<ActionDispatch::Request::Session:0x00007fb208019c00 ...>
raw_sessionおよびsession_serializerからsessionをdeleteして、
Railsのreset_session!メソッドを呼び出しているようです。
と思いきや、warden_compat.rbのメソッドらしいです。
> method(:reset_session!).source_location => ["/usr/local/bundle/gems/devise-4.8.0/lib/devise/rails/warden_compat.rb", 8]
module Warden::Mixins::Common def request @request ||= ActionDispatch::Request.new(env) end def reset_session! request.reset_session end def cookies request.cookie_jar end end
実際には ActionDispatch::Request クラスのreset_sessionメソッドが呼ばれているようです。
reset_session (ActionController::Base) - APIdock
raw_sessionについての知見が皆無なので調べてみます。
raw_session
先ほどstepで入った処理の中で色々実行してみました。
> raw_session.class => ActionDispatch::Request::Session > raw_session.methods => [:loaded?, :to_hash, :delete, :clear, :exists?, :to_h, ...] > raw_session.to_h => {"session_id"=>"b71549ad67aa9d67e27aa04fea2c0b37", "warden.user.user.key"=>[[1], "$2a$12$oLbqXNXAJ3P17TM.7bfaiu"], "warden.user.user.session"=>{"unique_session_id"=>"jj2nrehY8TNM-f_nrryY"}, "_csrf_token"=>"KbKDaYOjWm7jMLPfqctmdhG3ImpMGoQ4y9JX89plxLc="} > raw_session.method(:to_h).source_location => ["/usr/local/bundle/gems/actionpack-6.0.4.1/lib/action_dispatch/request/session.rb", 143]
action_dispatch/request/session.rb を見に行けばよさそう。
読んでもよくわからなかったので公式ドキュメントを見てみます、、、
Class: ActionDispatch::Request::Session — Documentation for rails (6.0.2.1)
Session is responsible for lazily loading the session from store.
セッションは、ストアからセッションを遅延ロードする責任があります。
出典: https://www.rubydoc.info/docs/rails/ActionDispatch/Request/Session
raw_sessionの役割についてはわかりましたが、定義元がよくわからないので調べてみます。
> method(:raw_session).source_location => ["/usr/local/bundle/gems/warden-1.2.9/lib/warden/mixins/common.rb", 9]
# Convenience method to access the session # :api: public def session env['rack.session'] end # session # Alias :session to :raw_session since the former will be user API for storing scoped data. alias :raw_session :session
env['rack.session']の返り値だったみたいです。
rack.sessionに値が書き込まれるタイミングはいつなのか?が気になりますが、返り値も定義元も役割もわかったので一旦ここまでにします。
session_serializerもいまいちわからんので調べます。
session_serializer
> session_serializer => #<Warden::SessionSerializer:0x00007f99501d5790 ... > method(:session_serializer).source_location => ["/usr/local/bundle/gems/warden-1.2.9/lib/warden/proxy.rb", 50]
def session_serializer @session_serializer ||= Warden::SessionSerializer.new(@env) end
コメントアウト部分を訳してみます。
Points to a SessionSerializer instance responsible for handling everything related with storing, fetching and removing the user session.
ユーザーセッションの保存、フェッチ、および削除に関連するすべての処理を担当するSessionSerializerインスタンスを指します。
出典: https://github.com/wardencommunity/warden/blob/master/lib/warden/proxy.rb#L46
CRUD全てを担当するクラスっぽいですね。定義元を見に行ってみます。
deleteメソッドの中身を見てみます。
def delete(scope, user=nil) session.delete(key_for(scope)) end # We can't cache this result because the session can be lazy loaded(セッションを遅延ロードするためにキャッシュを行いません) def session env["rack.session"] || {} end def key_for(scope) "warden.user.#{scope}.key" end
deleteメソッドはsessionから warden.user.user.key のようなkeyをdeleteするメソッドでした。
先ほど、「rack.sessionに値が書き込まれるタイミングはいつなのか?」と疑問を書いていましたが、storeメソッドが書き込んでいそうなので処理を追ってみます。
def store(user, scope) return unless user method_name = "#{scope}_serialize" specialized = respond_to?(method_name) session[key_for(scope)] = specialized ? send(method_name, user) : serialize(user) end
storeメソッドの呼び出し元がこちらでした。
def set_user(user, opts = {}) scope = (opts[:scope] ||= @config.default_scope) # Get the default options from the master configuration for the given scope opts = (@config[:scope_defaults][scope] || {}).merge(opts) opts[:event] ||= :set_user @users[scope] = user if opts[:store] != false && opts[:event] != :fetch options = env[ENV_SESSION_OPTIONS] if options if options.frozen? env[ENV_SESSION_OPTIONS] = options.merge(:renew => true).freeze else options[:renew] = true end end session_serializer.store(user, scope) end run_callbacks = opts.fetch(:run_callbacks, true) manager._run_callbacks(:after_set_user, user, self, opts) if run_callbacks @users[scope] end
set_userは_perform_authenticationというprivateメソッドで呼ばれています。
なので、「rack.sessionに値が書き込まれるタイミングはいつなのか?」という問いは、「_perform_authenticationメソッドでset_userが呼ばれた時」が一つの解かと思います。
ログアウトのことを調査していたのにログイン時の挙動の調査をしてしまいました、、、寄り道はここまでにします。
まとめ
destroyアクションが呼ばれると、
「Devise.sign_out_all_scopesが true なら、
全てのdeviseを使っている認証中のモデルのwardernのsessionを削除し、
falseなら引数に与えられたresourceのモデルのwardenのsessionを削除」
していました。
「wardenのsessionを削除」は、Warden::Proxy#logout メソッドが呼ばれていて、
内部では request.env['rack.session'].clear を呼んでsessionの中身をnilにすることでログアウトを実現してました。
今までログアウトについて、cookie初期化してsession削除してるんだろうな、程度の曖昧な理解に留まっていましたが、実際に処理を追ってみることで、
ログアウトの詳細な動作とwardenの仕組みの一端を理解することができました。
認証周りのコードを読めば読むほど、自分で認証周りを自作してはいけない気持ちになりますね。