deviseの調査メモ
Why?
「dev.toから学ぶRails」のコンテンツを作成する時に、deviseについての理解が曖昧だと気づいた。
なので、ちゃんと説明できるように調べてみることにした。
Goal
自分の中で浮かんだ疑問に対して自分の言葉で説明できるようになればOK。
疑問と応答
devise_for
って何?
devise_scope
って何?囲う必要あるの?
current_user
って具体的には何をしているの?
authenticate_user!
って具体的には何をしているの?
どのように定義され、参照できるかはcurrent_userと同じ。
定義は下記。
def authenticate_#{mapping}!(opts={})
opts[:scope] = :#{mapping}
warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end
wardenのauthenticate!
メソッド を呼び出している。
current_use
ではauthenticate
メソッドを呼び出していた。
内部の認証方法は同じなので説明は省略。
2つの違いは、authenticate!
メソッドは認証に失敗した場合にwardenのfailure_appが呼び出される。
authenticate
メソッドの方は単にnilを返す。
failure_appが呼び出されるコードは次のあたり。
sigin_in/sign_outの処理は具体的にどうなってるの?
omniauthの設定までなんでdeviseに対して行う必要があるの?ソーシャルログインはomniauthで完結できないの?
パスワード認証はどのように実装されてるの?
ここまでくればもう説明するまでもないですね。
ログイン認証画面のアクションは devise/sessions/create
です。
- warden.authenticate!
- validなstrategyとしてdatabase_authenticatableが実行される
- https://github.com/heartcombo/devise/blob/master/lib/devise/strategies/database_authenticatable.rb
- request.paramsの設定から対象のrecordを探すメソッド
find_for_database_authentication
が呼びだされる - recordがある(= 認証に成功)した場合はwardenの
success!(resource)
を呼び出すことで、認証された状態となる
Rack middlewareについて
deviseについて調べ始めると、deviseはwardenという認証周りのRack middlewareをベースとしていることが解った。
なので、rack middlewareがどういったものか調べてみた。
参考にした記事
- Rack入門 Rack Middleware編
- Rackとは何か
- Rackとは何か
- RailsのリクエストのライフサイクルとRackを理解する(翻訳)
- Understanding Rack and Rack Middleware
- Understanding Rack Apps and Middleware
Rackとは何か
RackはWebアプリケーションを実装するためのミニマムなインターフェースを提供している。
そのインターフェースを実装したプログラム(Rackアプリケーション)があれば、Webアプリケーションとして稼働させることができる。
Rackアプリケーションとは
以下 Rackとは何か から引用:
- callというメソッドを持っていること
- callメソッドの引数としてWebサーバからのリクエストを受けること
- callメソッドは,次の要素を含むレスポンスを返すること
-ステータスコード
-レスポンスヘッダ(Hash)
-レスポンスボディ(Array)
以下が引用先で紹介されているRackアプリケーションのサンプル:
# coding: utf-8
class SimpleApp
def call(env)
p env
case env['REQUEST_METHOD']
when 'GET'
[
200,
{ 'Content-Type' => 'text/html' },
['<html><body><form method="POST"><input type="submit" value="見たい?" /></form></body></html>']
]
when 'POST'
[
200,
{ 'Content-Type' => 'text/html' },
['<html><body>何見てんだよ</body></html>']
]
end
end
end
このクラスを、Rackの設定ファイルであるconfigu.ru
に設定すればWebアプリケーションとして稼働させられる。
# coding: utf-8
require 'simple_app.rb'
run SimpleApp.new
Rack middlewareとは
にとても解りやすくまとまっている。
deviseのコードを読んでいる時にenv
を使っているなあと思ってたけど、Rack middlewareにとってenv
が重要な要素だったということが解った。
まとめ
- middlewareもRackアプリケーション
- middlewareは初期化時に次のmiddlewareを設定される(チェーンされている)
- middlewareのcallメソッド次のような選択肢がある。
- envを編集して、次のmiddlewareのcallメソッドを呼び出す
- callのレスポンスをそのまま返す
- callのレスポンスを編集して返す
- 次のmiddlewareを使用せずに、レスポンスを返す
- envを編集して、次のmiddlewareのcallメソッドを呼び出す
Wardenについて
認証機能を実装するRack middleware
callメソッドを持つRackアプリケーションはWardern::Manager
クラスに実装されている。
scope
認証する対象としてscopeという概念がある。
userとかadminのような。
callメソッド内で行なっていること
-
env['warden']
に認証に使うWarden::Proxy
インスタンスをセット- deviseから使われる
authenticate
メソッドなどが実装されている
- deviseから使われる
-
チェインされた次のRack middlewareのcallメソッドを呼び出して、結果をresult変数に格納
-
認証に失敗している場合(status == 401)にfailure_appを呼び出す
データの持ち方
session
- sessionの操作は
env['rack.session']
オブジェクト経由で行う - sessionのキーは"warden.user.#{scope}.key"
session周りの処理はWarden::SessionSerializer
クラスに実装されている
sessionへのserialize, deserializeはconfig.serialize_into_session
で設定できる。
deviseもこの設定を行なっている。
上記から一部抜粋
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
mappingはDevise::Mappingインスタンス。
現状のroutesがどのscopeかを識別するために使われる(たぶん)
mapping.to
は、scopeがuserならUserクラスを表す。
serialize_into_session
メソッドはauthenticatable経由でモデルにincludeされる。
上記から一部抜粋
def serialize_into_session(record)
[record.to_key, record.authenticatable_salt]
end
proxy
認証されたuserのデータは proxyインスタンスのusersに設定される。
@users[scope] = user
この値が設定されているscopeは、以降認証されている状態として扱われる。
userのデータ
warden自体はuserの中身に関心がないので、どんなオブジェクトでもok(たぶん)
認証 authenticate
Warden::Proxy.authenticate などで認証ができる。
-
wardenに設定されているstrategiesの中から実行するstrategyを取得( = winning_strategy)
- 条件はscopeが適合したものと、 valid?(後述)なもの
- https://github.com/wardencommunity/warden/blob/master/lib/warden/proxy.rb#L353
-
winning_strategyがsuccessful?なら認証成功
- 認証されたユーザーを設定する
set_user(winning_strategy.user, opts.merge!(:event => :authentication))
strategyクラス
-
Warden::Strategies::Base
にから提供されるparams
などの値を用いて次のメソッドを実装する- valid? 認証するかどうか
- authenticate! 認証処理
- 認証が成功した場合は
success!(user)
のようにsuccess!
メソッドに対して認証したuserを渡す
- 認証が成功した場合は
詳しくは下記。
deviseで用意されているstrategiesは下記。
strategyの実装ファイル内で次のようにwardenに設定されている。
Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)
Rack session middleware について
-
Railsのsession周りコードリーディング
-
ActionDispatch::Session::RedisStore
やRack ::Session:: Abstract
までコードを追っている記事
-
deviseの読み込み、初期化
始点はlib/devise.rb
このファイルの下部で各種moduleをrequireしている。
require 'warden'
require 'devise/mapping'
require 'devise/models'
require 'devise/modules'
require 'devise/rails'
このファイルで定義されているmoduleDevise
は、各種configの値やdevise_for
経由で設定されたresourcesを保持している。
コアのmoduleといえる。
devise/mapping
resourceごとのroutesやmodulesの設定などを表すもの。
resourceとは、user
とかadmin
とかの認証の対象のことで、wardenのscopeと対応している。
mappingはconfig/routes.rb
で定義されるdevise_for
によって作成される。
例えば、devise_for :users
は"resoure = user"のmappingを作成していると表現できる。
また、mappingは、定義されたroutesでアクセスされた場合に、env["devise.mapping"]に格納される。
DeviseController
はこのmappingのデータを元にresourceの識別を行う。
devise/rails
devise_scope
やdevise_for
を定義しているlib/devise/rails/routes.rb
をrequireしている
このファイルを見れば、routes上でどういったAPIが使えるかが大体わかる。
まとめ
-
config/initializer/devise.rb
の設定を元に、各種moduleのactivateなどの初期化が行われる- この内容は
lib/devise.rb
に定義されているDevise moduleにまとめられる。
- この内容は
- routes上の
devise_for
によって、Devise::Mapping
インスタンスが作成される。- Devise moduleのmappingsクラス変数に格納される
- deviseのroutesにアクセスされた際に、env["devise.mapping"]に
Devise::Mapping
インスタンスが設定される - deviseの各種module, controllerはenv["devise.mapping"]の設定を元に、それぞれの機能を実行する
devise_forって何?
指定されたresourceに対するdevise向けのroutesを作成するもの。
devise向けのroutesとは、たとえば次のようなもの(resource=userの場合)
- ログイン画面
- name: new_user_session
- url: users/sign_in
- action: Devise::SessionsController#new
- ログアウト処理
- name: destroy_user_session
- url: users/sign_out
- action: Devise::SessionsController#destroy
作成されるroutesはmodelのdevise
メソッドで定義されたmoduleに必要なもの。
内部では、devise_scope
メソッドを使ってroutesが作成される。
devise_scope
メソッド経由で作成されたroutesは、実際にアクセスされた際にenv["devise.mapping"]
に対して、指定されたscopeのDevise::Mapping
インスタンスを設定されるようになっている。
devise_scope って何?囲う必要あるの?
deviseのroutesをカスタマイズしたいときに使うもの。
devise_for
で自動的に作成されるurlを使いたくない時などに。
例えばログイン画面のurlをデフォルトの/users/sign_in
ではなく、/sign_in
に変えたい場合は次のように指定する。
devise_scope :user do
get "sign_in", to: "devise/sessions#new"
end
また、devise_scope
はActionDispatch::Routing::Mapper
のconstraints
メソッドを使用することで、
実際にroutesにアクセスされた際に、request.env["devise.mapping"]
に指定されたscopeのDevise::Mapping
インスタンスを設定している。
これによって、各種DeviseController
を継承したControllerはどのscopeに対して処理するかを識別できる。
逆に言うと、複数のscopeに対して同じurlを指定することはできない。
なぜなら、Controllerがどのscopeに対する処理なのかが識別できなくなるから。
# NG 複数のscopeで同じurlを指定してしまっている
devise_scope :user do
get "sign_in", to: "devise/sessions#new"
end
devise_scope :admin do
get "sign_in", to: "devise/sessions#new"
end
囲う必要ってあるの?
devise_scope
で囲わずに、DeviseControllerを継承したアクションにroutesを設定すると、アクセス時にmappingが見つからないのでエラーになる。
current_user って具体的には何をしているの?
まず、どのように定義され、参照できるようになっているのか
Devise::Controllers::Helper
の define_helpers
メソッドで動的に定義されている。
define_helpers
メソッドはDevise
moduleのadd_mapping
メソッドから呼び出される。
add_mapping
メソッドはdevise_for
メソッド内から呼び出されるので、current_user
メソッドはRailsのroutesが初期化されるタイミングで定義されている。
HelperをActionControllerにincludeしているのは下記。
def self.include_helpers(scope)
ActiveSupport.on_load(:action_controller) do
include scope::Helpers if defined?(scope::Helpers)
include scope::UrlHelpers
end
ActiveSupport.on_load(:action_view) do
include scope::UrlHelpers
end
end
このように、Devise::Controllers::Helper
で定義されているメソッドはControllerから参照でき、
Devise::Controllers::UrlHelper
で定義されているメソッドはControllerとViewどちらからも参照できる。
では、具体的に何をしているのか?
動的に定義されるメソッドのコードは下記:
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
単純にwardenのauthenticate
メソッドを呼び出している。
wardenのauthenticate
メソッドの詳細は
の表題"認証 authenticate"のところにも書いてある。
warden.authenticateの処理
- usersプロパティに値が入っていればそれを返す
- sessionにuserのデータがあるかを探しに行く。データがあれば、deserializeして、usersプロパティにセットして、返す。
- 有効なstrategiesを実行し、認証が成功したら、そのuserを返す
- 有効かどうかの判定の多くは
reques.param
のデータに認証に必要なkeyがあるかで判断されるので、認証用のアクション以外では基本的に有効とはならない - 例えば
database_authenticatable
strategyでは、email
のkeyがあるかなど。
sigin_in/sign_outの処理は具体的にどうなってるの?
sign_in
代表的なアクションはdevise/sessions/create
だと思う。
まず、wadernのauthenticate!メソッドを呼び出して、結果をresourceに格納する:
self.resource = warden.authenticate!(auth_options)
感嘆符付きのauthenticate!メソッドなので、認証に失敗した場合は、failure_appを実行する処理に遷移するので、以降の処理は実行されない。
認証に成功した場合は、次に、Devise::Controllers::SignInOut
moduleに定義されているsign_in
メソッドを呼び出している。
sign_in
メソッドの内部ではwardenのset_user
メソッドを呼び出している。
authenticate!
メソッドでも内部でset_user
を呼び出しているの被っている処理はありそう。
sign_out
アクションはdevise/seissions/destroy
。
Devise::Controllers::SignInOut
moduleに定義されているsign_out
メソッドを呼び出している。
sign_out
メソッドはwardenのlogout
メソッドを呼び出している。
logout
メソッド内で、users
プロパティ、sessionから対象のscopeのuserデータを削除している。
まとめ
どちらも主な処理はwardenの対応するメソッドを呼び出している感じ。
deviseの認証系のコードはwardenをベースにしているから、結構薄くなっている感じ。
認証系以外のmoduleの方が重たそう。
omniauthの設定までなんでdeviseに対して行う必要があるの?ソーシャルログインはomniauthで完結できないの?
この質問は、deviseがomniauth周りの連携で何をやっているかを明確にすれば良さそう。
config
使用するサードパーティのproviiderと、providerごとのapp_id, secretなどの設定ができる。
routes
各scopeとproviderごとに、認証用とcallbackのroutesを作ってくださっている。
strategyのmiddleware設定
providerごとのstrategyのmiddlewareを設定してくださっている。
controller
omniauthのベースとして用意されているcontrollerはそれほど重たくない。
ただ、sign_inなどのdeviseのhelper系のメソッドを使えるのは大きそう。
まとめ
omniauthの機能をdeviseと共に自然に、簡単に使えるように細かいことをやってくださっている。
ちょこっとconfigを書くだけで、ここまで簡単にソーシャルログインを実装できるのの大部分はdeviseの仕事のおかげ。
モデルへの各種moduleの機能の読み込み
Devise::Models#devise
メソッドに使いたいmoduleを指定することで、モデルに各moduleに必要な機能を読み込んでいる。
Authenticatableモジュールは認証用のベースのものとして、デフォルトでincludeされる。
Devise::Model
自体は、ActiveSupport.on_load
を使ってActiveRecord
に対してextendされている。
初期化時の実行順序としては、
-
Model#devise
メソッドが実行されて、Modelに対してmodulesが設定される -
devise_for
メソッドで、対象のModelのmodules設定を元にroutesを作成する
moduleについてのメモ
- authenticatable
- Comfirmableの
active_for_authentication?
がいい例?
- Comfirmableの
- database_authenticatable
- confirmable
- registable
- modelは薄く、routesが主。
- Recoverable
- Confirmableのpasswardバージョン的な
- Rememberable
- strategyとセット
- strategyはデフォルトでセットされて、valid?の判定時にrememberableモジュールが使われているかどうかを見ている
- remember_me!を呼び出すことで、tokenと時間が入れ込まれる
- strategyとセット
- Trackable
- track処理は lib/devise/hooks/trackable.rb 場で、wardenの
after_set_user
経由で呼び出されるようになっている
- track処理は lib/devise/hooks/trackable.rb 場で、wardenの
- Timeoutable
- 使っているのは lib/devise/hooks/timeoutable.rb 場で wardenのafter_set_userから。
- Lockable
- active_for_authentication?
- 時間経過でのunlock処理は valid_for_authentication? 上で行なっている