もふもふ技術部

IT技術系mofmofメディア

devise の使い方(helpers, Database Authenticatable, Registerable)

Deviseの使い方(各種オプション編1)

devise gemはモジュールの概念に基づいて作成されています。 この性質のおかげで開発者は自分のアプリに本当に必要なものだけをピンポイントに導入することが可能です。使用していないコードをアプリに組み込まずに済むのは可読性の面からとてもありがたいですね。

※ここではマッピングされたモデルはUserであると仮定して話を進めていきます。

※また、Deviseはwardenという認証の仕組みをベースに採用していますが、今回Wardenについては深く触れません。

wardengemはこちら

各種モジュール

Deviseは大きく10のモジュールから構成されています。

  1. Database Authenticatable
  2. Registerable
  3. Validatable
  4. Recoverable
  5. Rememberable
  6. Confirmable
  7. Lockable
  8. Timeoutable
  9. Trackable
  10. Omniauthable

今回はhelpersモジュールに定義されている汎用メソッドと Database Authenticatable, Registerableの2つを利用した時に使えるメソッドを解説していきます。

deviseをインストールすることで使用可能になる汎用メソッド

deviseを実装するとhelpersモジュールに定義されているメソッドを使用することが可能になります。deviseを利用する場合はここに定義されているメソッドは必ずと言っていいほど使用するので、こちらを最初に解説します。

authenticate_user!
user_signed_in?
current_user
user_session

authenticate_user!

文字通りユーザーの認証を行うメソッド。

before_action :authenticate_user!

というように、あるリソースにアクセスできるかどうかを認証によって判定する時に使用されることが多い。

deviseのソースは下記

View on GitHub

def authenticate_#{mapping}!(opts={})

  opts[:scope] = :#{mapping}

  warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)

end

認証が走るのはdevise_controller 判定がfalseの時か、オプションでforceキーが与えられている時に認証が行われる。

current_user

多分deviseを使う上で一番お世話になるメソッド。 ログイン済みのユーザー情報を取得できるので大体これ使っとけば良い。

View on GitHub

def current_#{mapping}
  @current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end

mappingというのはここではuserが入る.なので@current_#{mapping}は今回は@current_userになると読み替えてください。

やっていることとしては、@current_userがtrueであれば@current_userを返却し、なければwarden.authenticate(:user)として認証を行いその値を@current_userに入れる。所謂nilガード構文。

使い方としてはログインしたユーザーのインスタンスがこのメソッドで返却されるので、

current_user.email   #=> 今ログインしているユーザーのメールアドレス

このような感じで使用できる。

user_signed_in?

Userがログインしているかどうかの判定に用いられることの多いメソッド。 実態は 上記のcurrent_userメソッドを二重否定している

!!current_user

ので、current_user判定でも同様のことが実行できる。コード内にあれば明示的にログインしているかどうか判断したいと一目でわかるので意思表示にもおすすめ。

user_session

current_userとwarden.session(:user)を判定してhash型の値のリストを返します。timeoutableモジュールとか使う時にお世話になるかも。

{‘last_logged_in_at’ => 1586134613482}

のような感じでUTCの時刻が入ってたりします。(例)

View on GitHub

 def #{mapping}_session
   current_#{mapping} && warden.session(:#{mapping})
 end

Database Authenticatable

password をハッシュ化し、ログイン中のユーザーの信頼性を検証するモジュール。デフォルトで有効になっている。

クラスメソッド

required_fields(klass) ⇒ Object

該当モデルに対して何がauthenticate上必須なのかを出力するメソッド。 デフォルトでは:encrypted_password, :email]が出力される。 固定で:encrypt_passwordが設定されており、それに加えてKlass.authentication_keysが追加された結果が出力される。 これは各モジュールに同様のクラスメソッドが定義されている。また、各モジュールには同名のクラスメソッドが用意されており、各モジュールで必要なものがあればオーバーライドされて使用される。

インスタンスメソッド

after_database_authentication ⇒ Object

インスタンスが認証に成功した後に呼ばれるコールバックメソッド。 ユーザーが認証に成功した後になんらかの処理を行いたい場合に特定の処理を行いたい時にこのメソッドの中にロジックを定義する。

View on GitHub

def after_database_authentication
  self.update_attribute(:invite_code, nil)
end

authenticatable_salt ⇒ Object

実装方法を問わずソルト値を取得できる。 ソルト値で比較検討したい時とかに使える

View on GitHub

def authenticatable_salt
  encrypted_password[0,29] if encrypted_password
end

実行結果はこんな感じ。

current_user.authenticatable_salt
#=> "$2a$11$oUYu5DBb2G9NTQ5bG9q4lu"

clean_up_passwords ⇒ Object

passwordとpassword_confirmationの値をnilにする。通常では新規登録に失敗した時に呼ばれる。登録失敗時にpasswordが空欄になるのはこのメソッドが呼ばれるから。

View on GitHub

def clean_up_passwords
  self.password = self.password_confirmation = nil
end

destroy_with_password(current_password) ⇒ Object

現在のパスワードを引数にとり、それがマッチしている場合はレコードを削除する。戻り値はbool型。

View on GitHub

def destroy_with_password(current_password)
  result = if valid_password?(current_password)
    destroy
  else
    valid?
    errors.add(:current_password, current_password.blank? ? :blank : :invalid)
    false
  end

  result
end

password=(new_password) ⇒ Object

与えられた値からハッシュされたパスワードを生成する。ハッシュ化されたパスワードはencrypted_passwordカラムに保存される。

View on GitHub

def password=(new_password)
  @password = new_password
  self.encrypted_password = password_digest(@password) if @password.present?
end

password_digest(password)⇒ Object (protected)

bycrytを使用してパスワードをハッシュ化する。もし独自のアルゴリズムを使用したい場合はこのメソッドをオーバーライドする。password=(new_password)に使用されているprotectedのメソッド。

View on GitHub

def password_digest(password)
  Devise::Encryptor.digest(self.class, password)
end

send_email_changed_notification ⇒ Object

メールアドレス変更の通知を送る。通知の送り先は変更前のメールアドレス。 View on GitHub

def send_email_changed_notification
  send_devise_notification(:email_changed, to: email_before_last_save)
end

skip_email_changed_notification! ⇒ Object

send_email_changed_notification のpassword版。登録されているemailアドレスにパスワードが変更された旨の通知を送る。 View on GitHub

def send_password_change_notification
  send_devise_notification(:password_change)
end

skip_email_changed_notification! ⇒ Object

emailのカラム更新後に通知を送らないようにする。 View on GitHub

def skip_email_changed_notification!
  @skip_email_changed_notification = true
end

skip_password_change_notification! ⇒ Object

passwordのカラム更新後に通知を送らないようにする。 View on GitHub

def skip_password_change_notification!
  @skip_password_change_notification = true
end

update_with_password(params, *options) ⇒ Object

入力されたパスワードがマッチしていれば(current_password属性)レコードを更新する。そうでない場合はfalseが返る。 パスワードを変更する場合など、現在のパスワード・新しいパスワード・確認用の新しいパスワード を入れる時などに呼ばれるメソッド。現在のパスワードを要求したいときの処理に使われる。 このメソッドではcurrent_passwordを入力することで例えばnameカラムなどを変更することができる。passwordフィールドとpassword_confirmカラムがformに存在する場合、片方だけの記述を行うとバリデーションに引っかかって更新できない。また、両方にcurrent_password以外の有効な値を入れる(つまり、passwordの更新を行う)場合、ログアウト処理が走るので、注意。current_password, password, password_confirmationに同じ値が入っている場合、更新処理が走り、ログアウトもされない。

View on GitHub

def update_with_password(params, *options)
  if options.present?
    ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc
      [Devise] The second argument of `DatabaseAuthenticatable#update_with_password`
      (`options`) is deprecated and it will be removed in the next major version.
      It was added to support a feature deprecated in Rails 4, so you can safely remove it
      from your code.
    DEPRECATION
  end

  current_password = params.delete(:current_password)

  if params[:password].blank?
    params.delete(:password)
    params.delete(:password_confirmation) if params[:password_confirmation].blank?
  end

  result = if valid_password?(current_password)
    update(params, *options)
  else
    assign_attributes(params, *options)
    valid?
    errors.add(:current_password, current_password.blank? ? :blank : :invalid)
    false
  end

  clean_up_passwords
  result
end

update_without_password(params, *options) ⇒ Object

パスワードの要求なしでカラムを変更する。上記の逆版。パスワードがないと変更したくない値がある場合、このメソッドをオーバーライドして保護する必要がある。

View on GitHub

def update_without_password(params, *options)
  if options.present?
    ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc
      [Devise] The second argument of `DatabaseAuthenticatable#update_without_password`
      (`options`) is deprecated and it will be removed in the next major version.
      It was added to support a feature deprecated in Rails 4, so you can safely remove it
      from your code.
    DEPRECATION
  end

  params.delete(:password)
  params.delete(:password_confirmation)

  result = update(params, *options)
  clean_up_passwords
  result
end

valid_password?(password) ⇒ Boolean

引数にpasswordを渡し、そのパスワードがユーザーパスワードであるかどうかを確認する。戻り値はboolean View on GitHub

def valid_password?(password)
  Devise::Encryptor.compare(self.class, encrypted_password, password)
end

Registerable

新しいリソースの登録に関する全ての責任を負う。 なんとこいつが提供してくれるインスタンスメソッドはない。 OAuthなどで利用される。

model定義はこちら。

# frozen_string_literal: true

module Devise
  module Models
    # Registerable is responsible for everything related to registering a new
    # resource (ie user sign up).
    module Registerable
      extend ActiveSupport::Concern

      def self.required_fields(klass)
        []
      end

      module ClassMethods
        # A convenience method that receives both parameters and session to
        # initialize a user. This can be used by OAuth, for example, to send
        # in the user token and be stored on initialization.
        #
        # By default discards all information sent by the session by calling
        # new with params.
        def new_with_session(params, session)
          new(params)
        end
      end
    end
  end
end

こちらの記事では以上です。他のモジュールについても、こちらの記事で紹介しています。