もふもふ技術部

IT技術系mofmofメディア

devise の使い方(Validatable Recoverable Rememberable)

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](https://github.com/heartcombo/devise/blob/master/lib/devise/models/recoverable.rb#L77)

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](https://github.com/heartcombo/devise/blob/master/lib/devise/models/recoverable.rb#L50)

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](https://github.com/heartcombo/devise/blob/master/lib/devise/models/recoverable.rb#L98)

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](https://github.com/heartcombo/devise/blob/master/lib/devise/models/recoverable.rb#L89)

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
<a id="anc3"></a>

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

インスタンスメソッド

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

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

  [View on GitHub](https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L100)

def after_remembered end

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

  [View on GitHub](https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L69)

def extend_remember_period self.class.extend_remember_period end

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

[View on GitHub](https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L58)

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](https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L65)

def remember_expires_at self.class.remember_for.from_now end

### remember_me!⇒ Object
forget_me!の逆版。`remember_token`と`remember_created_at`をバリデーションなしで更新する。

[View on GitHub](https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L50)

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](https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L103)

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](https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L86)

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](https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L73)

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

こちらの記事では以上です。他のモジュールについては、[こちらの記事](devise-option-methods-integrate)でも解説しています。