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

devise gemはモジュールの概念に基づいて作成されています。

devise

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

※ここではマッピングされたモデルは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

Validatable

ユーザーの電子メールとパスワードに関して必要な全ての検証を作成する。もし自分で検証を作成したい場合はオプションとなる。電子メールに関して存在すること、ユニークであること、形式が有効であること。パスワードに関しても存在すること、長さに関しても検証する。 直接使えるようになるインスタンスメソッドはない。オプションは/config/initializers/devise.rbで変更可能。デフォルトではemailのフォーマットに関するバリデーションと、パスワードの長さに関するバリデーションを設定できる。

インスタンスメソッド

email_required? ⇒ Boolean (protected)

バリデーションにemailの検証が必要かどうかを検証するメソッド。デフォルトでtrueを返すようになっている。email以外で検証を行いたい場合はこのメソッドをオーバーライドするか(protectedメソッドとして定義されているので注意)、そもそもValidatableをオフにすると良さそう。

View on GitHub

def email_required?
  true
end

password_required? ⇒ Boolean (protected)

passwordが必要かを確認するメソッド。新しいレコードである、あるいはパスワードが設定されている(入力フォームなどに入力されている)、パスワードの確認が必要な状態の場合、このメソッドはTrueを返す。 View on GitHub

def password_required?
  !persisted? || !password.nil? || !password_confirmation.nil?
end

Recoverable

パスワードのリセットを実行し、再設定の指示を行うモジュール。

インスタンスメソッド

clear_reset_password_token ⇒ Object (protected)

パスワードリセット用のtokenと送信日を記載したレコードの値をnilにする。

View on GitHub

def clear_reset_password_token
  self.reset_password_token = nil
  self.reset_password_sent_at = nil
end

clear_reset_password_token? ⇒ Boolean (protected)

View on GitHub

def clear_reset_password_token?
  encrypted_password_changed = respond_to?(:will_save_change_to_encrypted_password?) && will_save_change_to_encrypted_password?
  authentication_keys_changed = self.class.authentication_keys.any? do |attribute|
    respond_to?(“will_save_change_to_#{attribute}?”) && send(“will_save_change_to_#{attribute}?”)
  end

  authentication_keys_changed || encrypted_password_changed
end

will_save_change_to_encrypted_password?というメソッドを定義していた場合、その結果、あるいは authenticatableを有効にすると使えるようになるauthentication_keysの中で同様に定義されているメソッドの結果のどちらかがTrueの場合に戻り値としてtrueを返す。

reset_password(new_password, new_password_confirmation) ⇒ Object

パスワードをリセットして、新しいパスワードでレコードを更新するメソッド。パスワードが有効かつ、レコードが保存済みの場合はカラムを書き換え、そうでなければエラーを加えてfalseを返す。

View on GitHub

def reset_password(new_password, new_password_confirmation)
  if new_password.present?
    self.password = new_password
    self.password_confirmation = new_password_confirmation
    save
  else
    errors.add(:password, :blank)
    false
  end
end

reset_password_period_valid? ⇒ Boolean

リセットパスワードのトークンの送信日が制限期間内かどうかを検証する。 検証方法としては送信日と確認日(実行日)の差がreset_password_withinで設定された時間を超えていないかを確認する。reset_password_sent_at カラムが存在しない場合は常にtrueを返す。有効期限であるreset_password_withinは常に整数値である必要がある。この値はconfig/initializers/devise.rbに設定されており、デフォルトは6.hours。

以下はいくつかのパターンにおける例

# reset_password_within = 1.day and reset_password_sent_at = today
reset_password_period_valid?   # returns true

# reset_password_within = 5.days and reset_password_sent_at = 4.days.ago
reset_password_period_valid?   # returns true

# reset_password_within = 5.days and reset_password_sent_at = 5.days.ago
reset_password_period_valid?   # returns false

# reset_password_within = 0.days
reset_password_period_valid?   # will always return false

定義は以下 View on GitHub

def reset_password_period_valid?
  reset_password_sent_at && reset_password_sent_at.utc >= self.class.reset_password_within.ago.utc
end

send_reset_password_instructions ⇒ Object

リセットパスワードトークンをリセットして登録済みのemailアドレスにトークンを添付してメールを送信する。戻り値はtoken

View on GitHub

def send_reset_password_instructions
  token = set_reset_password_token
  send_reset_password_instructions_notification(token)

  token
end

send_reset_password_instructions_notification(token) ⇒ Object (protected)

send_reset_password_instructionsのメール送信担当。トークンを引数にとってメールを送信する部分を担当している。

View on GitHub

def send_reset_password_instructions_notification(token)
  send_devise_notification(:reset_password_instructions, token, {})
end

set_reset_password_token ⇒ Object (protected)

end_reset_password_instructionsのトークンリセット担当。 トークンを再発行し、送信日を設定してバリデーションを無視して保存する。

View on GitHub

def set_reset_password_token
  raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)

  self.reset_password_token   = enc
  self.reset_password_sent_at = Time.now.utc
  save(validate: false)
  raw
end

ちなみに実際にトークンを生成しているのはdevise/lib/devise/token_generator.rbのこの部分。被らないtokenが生成されるまでloop処理を行う。

def generate(klass, column)

      key = key_for(column)

      loop do

        raw = Devise.friendly_token

        enc = OpenSSL::HMAC.hexdigest(@digest, key, raw)

        break [raw, enc] unless klass.to_adapter.find_first({ column => enc })

      end

    end

Rememberable

保存されたCookieからユーザーを記憶するためのトークンの生成と削除を管理する。また、Cookieにシリアライズして保存し、保存された情報に基づいてレコードを検索するメソッドを提供する。 remember_meがatter_accessorで使用可能になる。

インスタンスメソッド

after_remembered ⇒ Object

正常に記憶処理が走った場合にコールバックされる。remember_me!した後に何かしらの独自処理を行いたい場合このコールバックメソッド内に実装を行う。

定義内には何も書かれていない

View on GitHub

def after_remembered
end

extend_remember_period ⇒ Object

cookieを用いてtokenの有効期限をアクセスがあった場合に延長するかどうかの設定。config/initializers/devise.rbで設定可能。デフォルトはfalse

View on GitHub

def extend_remember_period
  self.class.extend_remember_period
end

forget_me! ⇒ Object

remember tokenを削除し(もしあれば)、バリデーションなしで更新をかける。後述のremember_me!の逆版。

View on GitHub

def forget_me!
  return unless persisted?
  self.remember_token = nil if respond_to?(:remember_token)
  self.remember_created_at = nil if self.class.expire_all_remember_me_on_sign_out
  save(validate: false)
end

remember_expires_at ⇒ Object

再度検証要求されるまでにどれだけの期間だけユーザー情報を記憶するのかを決定する。config/initializers/devise.rbでカスタマイズ可能。デフォルトは2週間。 個別に定義したい時はremember_forをオーバーライドする。

View on GitHub

def remember_expires_at
  self.class.remember_for.from_now
end

remember_me!⇒ Object

forget_me!の逆版。remember_tokenremember_created_atをバリデーションなしで更新する。

View on GitHub

def remember_me!
  self.remember_token ||= self.class.remember_token if respond_to?(:remember_token)
  self.remember_created_at ||= Time.now.utc
  save(validate: false) if self.changed?
end

remember_me?(token, generated_at) ⇒ Boolean

インスタンスが渡されたトークンと有効期限をもって記憶状態にあるのかを検証するメソッド。 トークン(あるいはソルト値)と任意の時刻を引数にとる。 モデルに設定された記憶期間分前(2.weeksであれば現在から2週間前)よりも渡された時刻が新しいこと、かつ、remember_created_atの時間か現在の時間よりも渡された時刻が新しいものだった場合にrememberable_valueと渡されたトークン値の合致をsecure_compireにかけて行う。

secure_compireとは検証する時間でトークンやパスワードが大体どのくらいあっているのかを判断してパスワードなどを解明しようとするTimingAttackを防ぐための処理。

View on GitHub

def remember_me?(token, generated_at)
  # TODO: Normalize the JSON type coercion along with the Timeoutable hook
  # in a single place https://github.com/heartcombo/devise/blob/ffe9d6d406e79108cf32a2c6a1d0b3828849c40b/lib/devise/hooks/timeoutable.rb#L14-L18
  if generated_at.is_a?(String)
    generated_at = time_from_json(generated_at)
  end

  # The token is only valid if:
  # 1. we have a date
  # 2. the current time does not pass the expiry period
  # 3. the record has a remember_created_at date
  # 4. the token date is bigger than the remember_created_at
  # 5. the token matches
  generated_at.is_a?(Time) &&
   (self.class.remember_for.ago < generated_at) &&
   (generated_at > (remember_created_at || Time.now).utc) &&
   Devise.secure_compare(rememberable_value, token)
end

rememberable_options ⇒ Object

View on GitHub

def rememberable_options
  self.class.rememberable_options
end

Cookieを使用する際の値を設定できる。:secure => true, :same_site => :noneとか。chrome80のデフォルトLax変更に対応するときにいじったりすることになるかも。 config/initializers/devise.rbで編集可能。

rememberable_value ⇒ Object

remember_tokenか設定されてなければsalt値を返す。メソッドが呼べなかったり、ソルト値がnilだった場合は例外を投げる。

View on GitHub

def rememberable_value
  if respond_to?(:remember_token)
    remember_token
  elsif respond_to?(:authenticatable_salt) && (salt = authenticatable_salt.presence)
    salt
  else
    raise "authenticatable_salt returned nil for the #{self.class.name} model. " \
      "In order to use rememberable, you must ensure a password is always set " \
      "or have a remember_token column in your model or implement your own " \
      "rememberable_value in the model with custom logic."
  end
end

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