Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Devise-4.3.0の処理の流れ

More than 3 years have passed since last update.

ちょっと調べるたびに毎回処理の流れを追っていくのは大変なのでメモっておく。

対象

  • devise-4.3.0
  • warden-1.2.7

Warden::Managerの初期化

Devise::Railsで初期化。Warden::Managerの初期化時にDevise.warden_configを設定する

devise/blob/v4.3.0/lib/devise/rails.rb
module Devise
  class Engine < ::Rails::Engine
    # Initialize Warden and copy its configurations.
    config.app_middleware.use Warden::Manager do |config|
      Devise.warden_config = config
    end
  end
end

Warden::Manager#initializeにブロックを渡すとWarden::Configのインスタンスを引数に渡して実行してくれる。

warden/blob/v1.2.7/lib/warden/manager.rb
module Warden
  class Manager
    # Initialize the middleware. If a block is given, a Warden::Config is yielded so you can properly
    # configure the Warden::Manager.
    # :api: public
    def initialize(app, options={})
      default_strategies = options.delete(:default_strategies)

      @app, @config = app, Warden::Config.new(options)
      @config.default_strategies(*default_strategies) if default_strategies
      yield @config if block_given?
      self
    end
  end
end

ORMの設定(Devise::Models#devise)

config/initializers/devise.rbで各種ORM向けのdeviseの設定を読み込む

devise/blob/v4.3.0/lib/generators/templates/devise.rb
Devise.setup do |config|
  # ==> ORM configuration
  # Load and configure the ORM. Supports :active_record (default) and
  # :mongoid (bson_ext recommended) by default. Other ORMs may be
  # available as additional gems.
  require 'devise/orm/<%= options[:orm] %>'
end

ActiveRecordであればこんな感じ。

devise/blob/v4.3.0/lib/devise/orm/active_record.rb
require 'orm_adapter/adapters/active_record'

ActiveSupport.on_load(:active_record) do
  extend Devise::Models
end

Devise::Models#deviseでDeviseでが利用するためのMappingの設定を作成する

devise/blob/v4.3.0/lib/devise/models.rb
module Devise
  module Models
    # Include the chosen devise modules in your model:
    #
    #   devise :database_authenticatable, :confirmable, :recoverable
    #
    # You can also give any of the devise configuration values in form of a hash,
    # with specific values for this model. Please check your Devise initializer
    # for a complete description on those values.
    #
    def devise(*modules)
      options = modules.extract_options!.dup

      selected_modules = modules.map(&:to_sym).uniq.sort_by do |s|
        Devise::ALL.index(s) || -1  # follow Devise::ALL order
      end

      devise_modules_hook! do
        include Devise::Models::Authenticatable

        selected_modules.each do |m|
          mod = Devise::Models.const_get(m.to_s.classify)

          if mod.const_defined?("ClassMethods")
            class_mod = mod.const_get("ClassMethods")
            extend class_mod

            if class_mod.respond_to?(:available_configs)
              available_configs = class_mod.available_configs
              available_configs.each do |config|
                next unless options.key?(config)
                send(:"#{config}=", options.delete(config))
              end
            end
          end

          include mod
        end

        self.devise_modules |= selected_modules
        options.each { |key, value| send(:"#{key}=", value) }
      end
    end
  end
end

Mappingの設定

DeviseはActionDispatch::Routing::Mapperを拡張してdevise_forを定義している。

devise/blob/v4.3.0/lib/devise/rails/routes.rb
module ActionDispatch::Routing
  class Mapper
    def devise_for(*resources)
      @devise_finalized = false
      raise_no_secret_key unless Devise.secret_key
      options = resources.extract_options!

      options[:as]          ||= @scope[:as]     if @scope[:as].present?
      options[:module]      ||= @scope[:module] if @scope[:module].present?
      options[:path_prefix] ||= @scope[:path]   if @scope[:path].present?
      options[:path_names]    = (@scope[:path_names] || {}).merge(options[:path_names] || {})
      options[:constraints]   = (@scope[:constraints] || {}).merge(options[:constraints] || {})
      options[:defaults]      = (@scope[:defaults] || {}).merge(options[:defaults] || {})
      options[:options]       = @scope[:options] || {}
      options[:options][:format] = false if options[:format] == false

      resources.map!(&:to_sym)

      resources.each do |resource|
        mapping = Devise.add_mapping(resource, options)

        begin
          raise_no_devise_method_error!(mapping.class_name) unless mapping.to.respond_to?(:devise)
        rescue NameError => e
          raise unless mapping.class_name == resource.to_s.classify
          warn "[WARNING] You provided devise_for #{resource.inspect} but there is " \
            "no model #{mapping.class_name} defined in your application"
          next
        rescue NoMethodError => e
          raise unless e.message.include?("undefined method `devise'")
          raise_no_devise_method_error!(mapping.class_name)
        end

        if options[:controllers] && options[:controllers][:omniauth_callbacks]
          unless mapping.omniauthable?
            raise ArgumentError, "Mapping omniauth_callbacks on a resource that is not omniauthable\n" \
              "Please add `devise :omniauthable` to the `#{mapping.class_name}` model"
          end
        end

        routes = mapping.used_routes

        devise_scope mapping.name do
          with_devise_exclusive_scope mapping.fullpath, mapping.name, options do
            routes.each { |mod| send("devise_#{mod}", mapping, mapping.controllers) }
          end
        end
      end
    end
  end
end

mapping = Devise.add_mapping(resource, options)のresourceは:userみたいな値。

devise/blob/v4.3.0/lib/devise.rb
module Devise
  # Small method that adds a mapping to Devise.
  def self.add_mapping(resource, options)
    mapping = Devise::Mapping.new(resource, options)
    @@mappings[mapping.name] = mapping
    @@default_scope ||= mapping.name
    @@helpers.each { |h| h.define_helpers(mapping) }
    mapping
  end
end

DeviseのMappingの設定をWarden::Configに設定

RailsのActionDispatch::Routing::RouteSet#finalize!のタイミングで設定する。

devise/blob/v4.3.0/lib/devise/rails/routes.rb
module Devise
  module RouteSet
    def finalize!
      result = super
      @devise_finalized ||= begin
        # 省略

        Devise.configure_warden!
        Devise.regenerate_helpers!
        true
      end
      result
    end
  end
end

module ActionDispatch::Routing
  class RouteSet #:nodoc:
    # Ensure Devise modules are included only after loading routes, because we
    # need devise_for mappings already declared to create filters and helpers.
    prepend Devise::RouteSet
  end
end

Devise.mappingsの内容を元にWarden::Configscope_defaults, serialize_into_session, serialize_from_sessionの設定を行う。

devise/blob/v4.3.0/lib/devise.rb
module Devise
  # A method used internally to complete the setup of warden manager after routes are loaded.
  # See lib/devise/rails/routes.rb - ActionDispatch::Routing::RouteSet#finalize_with_devise!
  def self.configure_warden! #:nodoc:
    @@warden_configured ||= begin
      warden_config.failure_app   = Devise::Delegator.new
      warden_config.default_scope = Devise.default_scope
      warden_config.intercept_401 = false

      Devise.mappings.each_value do |mapping|
        warden_config.scope_defaults mapping.name, strategies: mapping.strategies

        warden_config.serialize_into_session(mapping.name) do |record|
          mapping.to.serialize_into_session(record)
        end

        warden_config.serialize_from_session(mapping.name) do |args|
          mapping.to.serialize_from_session(*args)
        end
      end

      @@warden_config_blocks.map { |block| block.call Devise.warden_config }
      true
    end
  end
end

Devise::Mapping#strategies(=Warden::Strategiesに登録されているstrategies)

Devise::Mapping#strategiesの定義

devise/blob/v4.3.0/lib/devise/mapping.rb
module Devise
  class Mapping #:nodoc:
    def strategies
      @strategies ||= STRATEGIES.values_at(*self.modules).compact.uniq.reverse
    end
  end
end

self.modulesDevise::Models#deviseに渡した引数のこと(:userからUserにして有効にしたモジュールを探し出している)。Devise::STRATEGIESDevise.add_modulesで更新される値。

devise/blob/v4.3.0/lib/devise.rb
module Devise
  def self.add_module(module_name, options = {})
    options.assert_valid_keys(:strategy, :model, :controller, :route, :no_input, :insert_at)

    ALL.insert (options[:insert_at] || -1), module_name

    if strategy = options[:strategy]
      strategy = (strategy == true ? module_name : strategy)
      STRATEGIES[module_name] = strategy
    end

    if controller = options[:controller]
      controller = (controller == true ? module_name : controller)
      CONTROLLERS[module_name] = controller
    end

    NO_INPUT << strategy if options[:no_input]

    if route = options[:route]
      case route
      when TrueClass
        key, value = module_name, []
      when Symbol
        key, value = route, []
      when Hash
        key, value = route.keys.first, route.values.flatten
      else
        raise ArgumentError, ":route should be true, a Symbol or a Hash"
      end

      URL_HELPERS[key] ||= []
      URL_HELPERS[key].concat(value)
      URL_HELPERS[key].uniq!

      ROUTES[module_name] = key
    end

    if options[:model]
      path = (options[:model] == true ? "devise/models/#{module_name}" : options[:model])
      camelized = ActiveSupport::Inflector.camelize(module_name.to_s)
      Devise::Models.send(:autoload, camelized.to_sym, path)
    end

    Devise::Mapping.add_module module_name
  end
end

devise/modules.rb(これはdevise.rbのなかでrequireされる)で組み立てている。

devise/blob/v4.3.0/lib/devise/modules.rb
require 'active_support/core_ext/object/with_options'

Devise.with_options model: true do |d|
  # Strategies first
  d.with_options strategy: true do |s|
    routes = [nil, :new, :destroy]
    s.add_module :database_authenticatable, controller: :sessions, route: { session: routes }
    s.add_module :rememberable, no_input: true
  end

  # Other authentications
  d.add_module :omniauthable, controller: :omniauth_callbacks,  route: :omniauth_callback

  # 省略
end

もう一度Devise::Mapping#strategiesの定義。

devise/blob/v4.3.0/lib/devise/mapping.rb
module Devise
  class Mapping #:nodoc:
    def strategies
      @strategies ||= STRATEGIES.values_at(*self.modules).compact.uniq.reverse
    end
  end
end

STRATEGIES=[:database_authenticatable, :remenberable]だがrevierseしてるので必ず[:remenberable, :database_authenticatable]の順になる。
Warden::Proxyで認証で:rememberableより前に:database_authenticatableが実行されることはない。

Deviseでの認証とログイン

wardenWarden::Proxyのインスタンス。

devise/blob/v4.3.0/lib/devise/controllers/helpers.rb
module Devise
  module Controllers
    # Those helpers are convenience methods added to ApplicationController.
    module Helpers
      # The main accessor for the warden proxy instance
      def warden
        request.env['warden'] or raise MissingWarden
      end
    end
  end
end

認証にはwarden.authenticate!を利用する

devise/blob/v4.3.0/lib/devise/controllers/helpers.rb
module Devise
  module Controllers
    # Those helpers are convenience methods added to ApplicationController.
    module Helpers
      module ClassMethods
        def devise_group(group_name, opts={})
          mappings = "[#{ opts[:contains].map { |m| ":#{m}" }.join(',') }]"

          class_eval <<-METHODS, __FILE__, __LINE__ + 1
            def authenticate_#{group_name}!(favourite=nil, opts={})
              unless #{group_name}_signed_in?
                mappings = #{mappings}
                mappings.unshift mappings.delete(favourite.to_sym) if favourite
                mappings.each do |mapping|
                  opts[:scope] = mapping
                  warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
                end
              end
            end

            # 省略
          METHODS
        end
      end
    end
  end
end

@configWarden::Config@config[:default_strategies][scope]でDeviseが設定したStrategiesが取得できる。

warden/blob/v1.2.7/lib/warden/proxy.rb
module Warden
  class Proxy
    def authenticate(*args)
      user, _opts = _perform_authentication(*args)
      user
    end

    def _perform_authentication(*args)
      scope, opts = _retrieve_scope_and_opts(args)
      user = nil

      # Look for an existing user in the session for this scope.
      # If there was no user in the session. See if we can get one from the request.
      return user, opts if user = user(opts.merge(:scope => scope))
      _run_strategies_for(scope, args)

      if winning_strategy && winning_strategy.successful?
        opts[:store] = opts.fetch(:store, winning_strategy.store?)
        set_user(winning_strategy.user, opts.merge!(:event => :authentication))
      end

      [@users[scope], opts]
    end

    # Run the strategies for a given scope
    def _run_strategies_for(scope, args) #:nodoc:
      self.winning_strategy = @winning_strategies[scope]
      return if winning_strategy && winning_strategy.halted?

      # Do not run any strategy if locked
      return if @locked

      if args.empty?
        defaults   = @config[:default_strategies]
        strategies = defaults[scope] || defaults[:_all]
      end

      (strategies || args).each do |name|
        strategy = _fetch_strategy(name, scope)
        next unless strategy && !strategy.performed? && strategy.valid?

        self.winning_strategy = @winning_strategies[scope] = strategy
        strategy._run!
        break if strategy.halted?
      end
    end
  end
end

ログインにはwarden.authenticatewarden.set_userを利用する。

devise/blob/v4.3.0/app/controllers/devise/sessions_controller.rb
class Devise::SessionsController < DeviseController
  # POST /resource/sign_in
  def create
    self.resource = warden.authenticate!(auth_options)
    set_flash_message!(:notice, :signed_in)
    sign_in(resource_name, resource)
    yield resource if block_given?
    respond_with resource, location: after_sign_in_path_for(resource)
  end
end
devise/blob/v4.3.0/lib/devise/controllers/sign_in_out.rb
module Devise
  module Controllers
    # Provide sign in and sign out functionality.
    # Included by default in all controllers.
    module SignInOut
      def sign_in(resource_or_scope, *args)
        options  = args.extract_options!
        scope    = Devise::Mapping.find_scope!(resource_or_scope)
        resource = args.last || resource_or_scope

        expire_data_after_sign_in!

        if options[:bypass]
          ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
          [Devise] bypass option is deprecated and it will be removed in future version of Devise.
          Please use bypass_sign_in method instead.
          Example:

            bypass_sign_in(user)
          DEPRECATION
          warden.session_serializer.store(resource, scope)
        elsif warden.user(scope) == resource && !options.delete(:force)
          # Do nothing. User already signed in and we are not forcing it.
          true
        else
          warden.set_user(resource, options.merge!(scope: scope))
        end
      end
    end
  end
end
warden/blob/v1.2.7/lib/warden/proxy.rb
module Warden
  class Proxy
    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]
        options[:renew] = true if options
        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
  end
end

lib/devise/hooks以下のファイル

Wardan::Hooksに定義してある拡張ポイントに対してメソッドを登録している。コールバックの呼び出しはWarden::Proxyの各種操作(authenticate!とかset_userとか)の実行のタイミングで呼び出されている。

troter
I'm a horse racing fan, a programmer, a mercurial evangelist in Japan. Live in #mercurialjp #TokyoMercurial .
http://troter.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away