普段RailsでWebアプリの開発をしていますが、使わない機能や内部の処理など知らないことは多いです。そこでDeviseで使う機能の実装をみて知見を増やそうかと思います。なぜ、Deviseかというとボリュームが多いGemが良かったのと、認証周りの詳しい動きが知りたかったからです。
概要
今回はauthenticate_user!
に焦点を当ててコードの中身を見ていこうと思います。authenticate_user
は認証済みのユーザーか確認するメソッドでDeviseで最もよく見るメソッドの1つかと思います。
またコードを読むDeviseのバージョンはv4.6.2
にしています。
define_helpers
authenticate_user!
の定義場所を探そうとしたら、まずはdefine_helpers
というメソッドに出会います。define_helpers
では認証で利用するメソッドの定義をしています。
def self.define_helpers(mapping) #:nodoc:
mapping = mapping.name
class_eval <<-METHODS, __FILE__, __LINE__ + 1
def authenticate_#{mapping}!(opts={})
opts[:scope] = :#{mapping}
warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end
def #{mapping}_signed_in?
!!current_#{mapping}
end
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
def #{mapping}_session
current_#{mapping} && warden.session(:#{mapping})
end
METHODS
class_eval
class_eval <<-METHODS, __FILE__, __LINE__ + 1
でヒアドキュメントとなっているところが定義部です。authenticate_user!
以外にもcurrent_user
などもここで定義しているようです。class_eval`はメタプログラミングで使われるメソッドでこれを使用すると引数に渡した文字列をメソッド定義のコードとして読んでくれます。
Devise利用では多くは認証のモデル名をUserにしますが、任意のモデル名にすることができます。その場合はauthenticate_***
と動的に定義しないのでclass_eval
を使ってメソッドを実装しています。
class_eval
を試しに使ってみた例です。
class DeviseMemo
def self.define_methods(mapping)
class_eval <<-METHODS, __FILE__, __LINE__ + 1
def authenticate_#{mapping}!
p "called authenticate_#{mapping}!"
end
def #{mapping}_signed_in?
p "called #{mapping}_signed_in?"
end
METHODS
end
end
DeviseMemo.define_methods("user")
devise_memo = DeviseMemo.new
devise_memo.authenticate_user!
devise_memo.user_signed_in?
$ ruby devise_memo.rb
"called authenticate_user!"
"called user_signed_in?"
warden
authenticate_user!
の中身は短い、というか結局はwardenというGemを使って認証確認しているようですね。
ここを見ないといけなくなりました。これは次の機会にしておきます。
def authenticate_#{mapping}!(opts={})
opts[:scope] = :#{mapping}
warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end
current_user
やuser_session
もwardenを使っているので、Deviseの根幹を握っているようです。
add_mapping
define_helpers
の呼び出し場所を確認してみるとlib/devise.rb
の中にあるadd_mapping
で呼ばれています。
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
ちなみにadd_mapping
はdevise_for
の中で呼ばれます。devise_for
も次の機会で詳しく中身を見ていきたいです。
def devise_for(*resources)
@devise_finalized = false
raise_no_secret_key unless Devise.secret_key
options = resources.extract_options!
(省略)
resources.each do |resource|
mapping = Devise.add_mapping(resource, options)
extract_options!
は可変長の引数を分解できるメソッドです。
下記が簡単な例です。extract_options!
はActiveSupportのメソッドなのでRailsコンソールで試すのが楽です。
def devise_for_test(*resources)
p resources
options = resources.extract_options!
p resources
p options
end
devise_for_test :users, controllers: [ sessions: 'users/sessions' ]
# 出力
[:users, {:controllers=>[{:sessions=>"users/sessions"}]}]
[:users]
{:controllers=>[{:sessions=>"users/sessions"}]}
devise_for
でモデル名とオプションに分けられてDevise.add_mapping(resource, options)
と呼び出されます。
そしてadd_mapping
内でmapping = Devise::Mapping.new(resource, options)
とマッピングを作成しています。
mapping
はdefine_helpers
でmapping = mapping.name
とname
だけが使われていましたが、Devise::Mapping
の中を覗いてみると、
class Mapping #:nodoc:
alias :name :singular
def initialize(name, options) #:nodoc:
@scoped_path = options[:as] ? "#{options[:as]}/#{name}" : name.to_s
@singular = (options[:singular] || @scoped_path.tr('/', '_').singularize).to_sym
となっています。
name
に注目してコードを繋げてみると、認証モデルがUserである場合は@singular = :users.to_s.tr('/', '_').singularize.to_sym
とみれます。singularize
は複数形を単数形に変換するメソッドで、最終的に@singular = :user
となりsingular
のエイリアスがname
となっているのでmapping.name
で:user
が取得できます。
するとdefine_methods
の引数に:user
が渡されauthenticate_user!
が出来上がるという流れになっています。
最後に
authenticate_user!
をみても普段使わないようなメソッドや手法がありなかなか有意義でした。これはDeviseのほんの一部なのでまだまだみるべきところはたくさんあります。今後もDeviseのコードリーディングで勉強していこうと思いますが、今日の学びからwardenの方を先に読んでいくことになりそうです。