LoginSignup
5
2

More than 1 year has passed since last update.

devise ログイン後の遷移先を指定する時に気になったことを深掘りしてみた。

Posted at

はじめに

deviseについて学習していたところ、ログイン後の遷移先を指定先についてのメソッドの挙動について気になったことがありましたので、詳しく調べてみました。

ログイン後の遷移先の指定方法

指定方法はいたって簡単で、application_controller.rbにafter_sign_in_path_for(resource)メソッドを定義するだけです。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  def after_sign_in_path_for(resource)
    pages_show_path
  end
end

上記の例の場合、ログイン後にルーティングで定義されているpages_show_pathに遷移するようになります。

どうしてこのメソッドでログイン後の遷移先を指定できるのか

Deviseのgemをinstallすると、ApplicationControllerにユーザー認証用の基本的なメソッドを付与します。
つまり、after_sign_in_path_forメソッドをApplicationControllerに再定義することで、メソッドのオーバーライドをしているのです。

参考:Deviseのモヤモヤを解消して快適なRailsライフを送ろう!

で、何が気になったのか

何が気になったのかというと、after_sign_in_path_forメソッドのデフォルトの挙動です。
deviseについて学習するために参考にさせていただいたQiitaの記事では、以下のように記載がありました。

ログインすると、デフォルトでは root_url に飛ばされます。

Devise側で定義されている、after_sign_in_path_forメソッドを確認したら、遷移先がroot_pathになるような処理がされているんだろうなと思い、実際に確認してみたところ、、、

lib/devise/controllers/helpers.rb
def after_sign_in_path_for(resource_or_scope)
  stored_location_for(resource_or_scope) || signed_in_root_path(resource_or_scope)
end

ん?なんか思っていたのと違う。。。
メソッドの説明を確認してみます。

By default, it first tries to find a valid resource_return_to key in the session, then it fallbacks to resource_root_path, otherwise it uses the root path
(デフォルトでは、セッション内の有効なresource_return_to keyキーを探します。resource_return_to keyキーが見つからなかった場合は、resource_root_pathにフォールバックし、それも見つからなかった場合は、root pathを使います。)

引用:Devise::Controllers::Helpers#after_sign_in_path_for

どうやら、デフォルトでは、すぐにroot_pathを使うのではなく、まず、sessionの情報を使って遷移先を決定するようです。
説明文の感じだと、sessionの中身は、URLの情報が入っていそうですね。

ということで、デフォルトの挙動をコードベースで確認してみたくなったので調べてみました。

コードベースで確認してみる

コードベースでも確認してみます。

lib/devise/controllers/helpers.rb
def after_sign_in_path_for(resource_or_scope)
  stored_location_for(resource_or_scope) || signed_in_root_path(resource_or_scope)
end

||は、左から順に評価し、一番最初に"true"になったものを返す演算子です。

つまり、処理はこんな感じです。

まず、stored_location_for(resource_or_scope)メソッドを実行。ここで実行結果が"true"であれば、処理終了。
実行結果が"false"だった場合、signed_in_root_path(resource_or_scope)を実行。

それでは、各メソッドの処理を確認していきましょう。

stored_location_for(resource_or_scope)メソッドについて

1.stored_location_for(resource_or_scope)メソッドの概要

まずコードを確認してみましょう。

lib/devise/controllers/store_location.rb
# File 'lib/devise/controllers/store_location.rb', line 18
def stored_location_for(resource_or_scope)
  session_key = stored_location_key_for(resource_or_scope)

  if is_navigational_format?
    session.delete(session_key)
  else
    session[session_key]
  end
end

処理としては、
session_keyという変数にstored_location_key_for(resource_or_scope)メソッドの戻り値を代入しています。
次に、is_navigational_format?の結果がtrue or falseで以下のように条件分岐をしています。
trueの場合:session[session_key]の情報を削除。
falseの場合:session[session_key]を戻り値として返す。

メソッドの説明を確認してみます。

Returns and delete (if it's navigational format) the url stored in the session for the given scope. Useful for giving redirect backs after sign up:
(引数として与えられたスコープにかかるsessionに保存された、urlを返します。もし、それがナビゲーションのフォーマットであれば、削除します。これは、サインアップ後のリダイレクトバック先を指定するのに便利です。)

引用:Devise::Controllers::StoreLocation#stored_location_for

sessionに保存されている情報がナビゲーションの情報であった場合は、sessionを削除。
sessionに保存されている情報がurlであった場合は、urlの情報を返す。

やはり、sessionに保存されている情報は、urlでしたね。

次に、sessionについて確認しましょう。

2.変数session_keyの値について

変数session_keyは、stored_location_key_for(resource_or_scope)メソッドの戻り値が代入されています。

それでは、stored_location_key_for(resource_or_scope)メソッドについて確認します。

devise/lib/devise/controllers/store_location.rb
  def stored_location_key_for(resource_or_scope)
        scope = Devise::Mapping.find_scope!(resource_or_scope)
        "#{scope}_return_to"

メソッドは、resourceもしくは、scopeを引数にしています。今回は、Userモデルにdeviseを実装しているので、引数にはuserが入ります。

変数scopeには、DeviseのMappingクラスのfind_scope!メソッドの戻り値が代入されます。

最終的に文字列"#{scope}_return_to"を返します。

変数scopeの値について

変数scopeの値を理解するためには、以下の2つを理解する必要があります。

  1. Devise::Mapping
  2. find_scope!(resource_or_scope)

1.DeviseのMappingクラスは、routes.rb内のdevise_forで設定された各リソースを元にマッピングオブジェクトを作成します。この時、マッピングオブジェクトの名前は、単数形の名詞になります。
ex.devise_for :users → user
Class: Devise::Mapping

2.find_scope!(resource_or_scope)メソッドは、deviseのscopeをシンボルの形式で返してくれるメソッドです。scopeが見つからなかった場合は、エラーを吐きます。

devise/lib/devise/mapping.rb
  # Receives an object and find a scope for it. If a scope cannot be found,
    # raises an error. If a symbol is given, it's considered to be the scope.
    def self.find_scope!(obj)
      obj = obj.devise_scope if obj.respond_to?(:devise_scope)
      case obj
      when String, Symbol
        return obj.to_sym
      when Class
        Devise.mappings.each_value { |m| return m.name if obj <= m.to }
      else
        Devise.mappings.each_value { |m| return m.name if obj.is_a?(m.to) }
      end

      raise "Could not find a valid mapping for #{obj.inspect}"
    end

内部的には、devise_scopeメソッドで作成されたdeviseのscopeをシンボルの形式で返しています。

この2つ結果を踏まえると、最終的な戻り値は、
:user_return_to
となります。

つまり、変数session_keyの値は、:user_return_toであり、
session[:user_return_to]となります。
sessionのキーとするためにシンボルにして値を返していたのですね。

session[:user_return_to]の値について

ここまでで、session[:user_return_to]に、urlの情報が保存されていて、ログイン後にこのurlに遷移するところまでわかりました。
次に生じる疑問点は、sessionではどのurlを保存しているのかです。

1.store_location_for(resource_or_scope, location)メソッドについて

stored_location_for(resource_or_scope)メソッドのすぐ下に以下のようなメソッドが定義されています。

devise/lib/devise/controllers/store_location.rb
# Stores the provided location to redirect the user after signing in.
      # Useful in combination with the `stored_location_for` helper.
      #
      # Example:
      #
      #   store_location_for(:user, dashboard_path)
      #   redirect_to user_facebook_omniauth_authorize_path
      #
      def store_location_for(resource_or_scope, location)
        session_key = stored_location_key_for(resource_or_scope)

        path = extract_path_from_location(location)
        session[session_key] = path if path
      end

Stores the provided location to redirect the user after signing in.
Useful in combination with the stored_location_for helper.
(ユーザーをサインイン後にリダイレクトさせるために引数で提供された場所を保存します。stored_location_for ヘルパーと一緒に使うと便利です。)

ログイン後の遷移先の情報を保存してくれるメソッドのようです。

処理を確認すると、、、、
pathという変数にextract_path_from_location(location)の戻り値が代入されています。そしてその変数pathが存在する場合にpathの値をsession[session_key]に代入しています。

つまり、extract_path_from_location(location)の戻り値こそが、ログイン後のリダイレクト先ということになります。

それでは、extract_path_from_location(location)メソッドについて確認しましょう。

   def extract_path_from_location(location)
        uri = parse_uri(location)

        if uri 
          path = remove_domain_from_uri(uri)
          path = add_fragment_back_to_path(uri, path)

          path
        end
      end

処理を確認すると、
引数の値をparse_uriメソッドでuriとして生成し、変数uriに代入します。
変数uriの値が存在する場合、pathとして機能するように変数の値を加工しています。
ここでふと思いました、『あれ、でも引数には何が入るんだろう』と。
現時点では、アプリケーション側では、store_location_for(resource_or_scope, location)メソッドを定義していませんので、引数を設定することはありません。
『ということは、store_location_for(resource_or_scope, location)メソッドをアプリケーション側で定義する必要があるのか。』
ということで、もう少し調べてみることにしました。

調べてみると、deviseのwikiに以下の記事がありました。
How To: Redirect back to current page after sign in, sign out, sign up, update

なるほど、やはり自分で定義してあげないといけないのか。と思った矢先に以下の記述に目が止まりました。

The following guides are already implemented in Devise 4.7 version(以下のガイダンスについては、Devise4.7バージョンで実装済みです。)

実装済み??ってことは、自分でメソッドを定義しなくても使えるってことなんでしょう。

2.store_location!メソッドについて

githubには便利な機能があって、コード間を楽々移動できちゃいます。この機能を使ってstore_location_for(resource_or_scope, location)メソッドを参照しているメソッドを探します。すると、devise/lib/devise/failure_app.rbの243列目に参照しているメソッドがありました。
スクリーンショット 2021-10-08 23.14.51.png
failure_app.rbは、ユーザーが認証に失敗した際の処理を記述したファイルです。
メソッドの説明を確認します。

devise/lib/devise/failure_app.rb
    # Stores requested URI to redirect the user after signing in. We can't use
    # the scoped session provided by warden here, since the user is not
    # authenticated yet, but we still need to store the URI based on scope, so
    # different scopes would never use the same URI to redirect.
    def store_location!
      store_location_for(scope, attempted_path) if request.get? && !http_auth?
    end

Stores requested URI to redirect the user after signing in. We can't use the scoped session provided by warden here, since the user is not authenticated yet, but we still need to store the URI based on scope, so different scopes would never use the same URI to redirect.(サインイン後にユーザーをリダイレクトさせるためのリクエストされたURIを保存します。ここでは、wardenで提供されたscoped sessionを使用することはできません。なぜなら、ユーザーは、まだ認証されていないからです。しかし、異なるscopeが同じURIをリダイレクト先として使用しないようにするために、scopeに基づいたURIを保存しておく必要があります。)

処理としては、
request.get?の結果が"true"でかつhttp_auth?の結果が"false"であった場合にstore_location_for(scope, attempted_path)を実行するというものです。

1.メソッドの制御について

  1. request.get?は、リクエストがHTTP GETメソッドであれば"true"を返すメソッドです。
  2. http_auth?は、ajaxによるリクエストであった場合に"true"を返すメソッドです。
devise/lib/devise/failure_app.rb
   # Choose whether we should respond in an HTTP authentication fashion,
    # including 401 and optional headers.
    #
    # This method allows the user to explicitly disable HTTP authentication
    # on AJAX requests in case they want to redirect on failures instead of
    # handling the errors on their own. This is useful in case your AJAX API
    # is the same as your public API and uses a format like JSON (so you
    # cannot mark JSON as a navigational format).
    def http_auth?
      if request.xhr?
        Devise.http_authenticatable_on_xhr
      else
        !(request_format && is_navigational_format?)
      end
    end

このメソッドの制御については、deviseのwikiには以下のようにstorable_location?メソッドとして定義されていました。

  private
    # Its important that the location is NOT stored if:
    # - The request method is not GET (non idempotent)
    # - The request is handled by a Devise controller such as Devise::SessionsController as that could cause an 
    #    infinite redirect loop.
    # - The request is an Ajax request as this can lead to very unexpected behaviour.
    def storable_location?
      request.get? && is_navigational_format? && !devise_controller? && !request.xhr? 
    end

    def store_user_location!
      # :user is the scope we are authenticating
      store_location_for(:user, request.fullpath)
    end
end

この説明書きによると以下の場合には、ロケーション情報が保存されないとのことです。

  1. リクエストがGETではないとき(常に同じ結果を返す(冪等)ものでないリクエスト)
  2. リクエストがDeviseのコントローラーによって制御されているもの(無限リダイレクトループを引き起こす)
  3. リクエストがAJAXによるものである場合(全く予期せぬ動作を引き起こす)

おそらく、store_location!メソッドのメソッド制御の部分がこれにあたるのでしょう。

2.store_location_forの引数について

メソッドの実行部分をもう一度見てみましょう。

store_location_for(scope, attempted_path)

引数には、以下の二つが設定されています。

  1. scope
  2. attempted_path
●引数scopeについて

scopeは、以下のように定義されています。

devise/lib/devise/failure_app.rb
  def scope
      @scope ||= warden_options[:scope] || Devise.default_scope
    end

処理としては、インスタンス変数scopeの値が存在しない場合、warden_options[:scope]を自己代入。それも無い場合は、Devise.default_scopeを自己代入するようです。

1. warden_options[:scope]

wardenとは、rubyで作成された、ウェブアプリケーションに認証メカニズムを提供するために設計されたRackベースのミドルうウェアです。
Deviseは、wardenベースで作られたものです。
それでは、warden_optionsの定義を確認してみましょう。

devise/lib/devise/failure_app.rb
 def warden_options
      request.respond_to?(:get_header) ? request.get_header("warden.options") : request.env["warden.options"]
    end

メソッドは三項演算子の形式になっています。
respond_to?メソッドで、request(レシーバー)にget_headerメソッドが定義されているかを確認し、定義されていれば、request.get_header("warden.options")を実行し、レシーバーに定義されていなかった場合は、request.env["warden.options"]を実行します。
次に、get_headerメソッドについて確認します。

lib/rack/request.rb
 # Get a request specific value for `name`.
    def get_header(name)
      @env[name]
    end

メソッドは、rackで定義されています。
リクエストを引数で与えられた名前のついた特定の値にするメソッドのようです。
request.env["warden.options"]と結局同じになるようですね。
つまり、このメソッドは、
"warden.options"という名前で保存されたリクエストの情報を返すメソッドのようです。
このリクエストは、認証失敗時にユーザーが行なったリクエストのことです。

ここまでのことを踏まえると、Deviseは、未ログインユーザーが認証失敗時にリクエストした情報を、env["warden.options"]として保存していることがわかります。

長くなりましたが、warden_options[:scope]には、現在devise認証を用いているモデルのuserが入ることになります。

2. Devise.default_scopeは、

routes.rbで一番最初に定義されたdevise_for :以下の部分がデフォルト値で、config/initializers/divise.rbで設定している場合は、それを使用します。今回は、userモデルのみしかdeviseを実装していませんので、userとなります。

2. warden_options[:attempted_path]

ここには、認証失敗時にユーザーがアクセスを試みたパスの情報が入っています。

devise/lib/devise/failure_app.rb
 def attempted_path
      warden_options[:attempted_path]
    end

参考:Deviseちょっとしたtips2つ

引数についてまとめます。 1. scope=Deviseでログイン失敗時に操作しようとしたモデル名(単数形) 2. attempted_path=ログイン失敗時にアクセスを試みたパス

devise/lib/devise/failure_app.rb
  def store_location!
      store_location_for(scope, attempted_path) if request.get? && !http_auth?
    end

rubyメソッドの引数は、値渡しです。
scopeとattempted_pathの定義された値がそのまま引数として渡されます。

Deviseでの認証失時、store_location_for(scope, attempted_path) では、以下のことが実行されます。 session[:user_return_to]=認証失敗時のパス情報

after_sign_in_path_forメソッドのデフォルト挙動(まとめ)

lib/devise/controllers/helpers.rb
def after_sign_in_path_for(resource_or_scope)
  stored_location_for(resource_or_scope) || signed_in_root_path(resource_or_scope)
end

after_sign_in_path_for(resource_or_scope)は、ログイン前に、認証に失敗しているか否かによって、ログイン後の遷移先が変わります。
具体的には、以下のようになります。

1. ログイン前に認証に失敗して、ログインページにリダイレクト後、ログインをした場合

→リダイレクト直前のページへ遷移。

2. ログインページからログインした場合

→リソースベースのルートページを定義していれば、そこへ。定義していなければ、ルートページへ遷移。

after_sign_in_path_forメソッドのデフォルトは、基本的には、ルートパスへの遷移です。しかし、ログインが必要なページ(仮にAページとします)に未ログイン状態でアクセスした場合については、ログインした後にAページに遷移させるということがわかりました。ちなみにこれをフレンドリーフォワーディングといいます。

参考:フレンドリーフォワーディング

おわりに

とてもシンプルに見えたコードでしたが、追っていくとなかなか複雑でした。
実際、コードリーディングをしてみましたが、知らないこと、わからなことばかりで大変な部分もありましたが、初学者の身からすると大変勉強になったと感じました。間違っている部分がございましたらご指摘いただけると幸いです。

参考

heartcombo/devise
Rails ガイド
How To: Redirect back to current page after sign in, sign out, sign up, update
warden wiki
[Rails] deviseの使い方(rails6版)
Deviseのモヤモヤを解消して快適なRailsライフを送ろう!
Deviseちょっとしたtips2つ

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2