ちょっと調べるたびに毎回処理の流れを追っていくのは大変なのでメモっておく。
対象
- devise-4.3.0
- warden-1.2.7
Warden::Managerの初期化
Devise::Rails
で初期化。Warden::Manager
の初期化時にDevise.warden_config
を設定する
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
のインスタンスを引数に渡して実行してくれる。
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.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であればこんな感じ。
require 'orm_adapter/adapters/active_record'
ActiveSupport.on_load(:active_record) do
extend Devise::Models
end
Devise::Models#devise
でDeviseでが利用するためのMappingの設定を作成する
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
を定義している。
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
みたいな値。
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!
のタイミングで設定する。
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::Config
のscope_defaults
, serialize_into_session
, serialize_from_session
の設定を行う。
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
の定義
module Devise
class Mapping #:nodoc:
def strategies
@strategies ||= STRATEGIES.values_at(*self.modules).compact.uniq.reverse
end
end
end
self.modules
はDevise::Models#devise
に渡した引数のこと(:user
からUser
にして有効にしたモジュールを探し出している)。Devise::STRATEGIES
はDevise.add_modules
で更新される値。
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される)で組み立てている。
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
の定義。
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での認証とログイン
warden
はWarden::Proxy
のインスタンス。
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!
を利用する
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
@config
はWarden::Config
。@config[:default_strategies][scope]
でDeviseが設定したStrategiesが取得できる。
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.authenticate
とwarden.set_user
を利用する。
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
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
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
とか)の実行のタイミングで呼び出されている。