LoginSignup
33
28

More than 5 years have passed since last update.

has_secure_passwordのvalidationsをカスタマイズする

Posted at

railsのhas_secure_passwordで自分の思い通りにvalidationを操作できないかと思って書いたメモ。

おさらい:has_secure_passwordとは?

ざっくり書くと、色んなwebサイトで使われている(そうな)パスワード認証周りの機能を実装してくれるメソッド。

公式ドキュメントではこのように説明されている。

ActiveModel::SecurePassword::ClassMethods

  • 用意したクラスにパスワードに関連する属性や検証を行うメソッドを付け加える。1
  • password属性に対して以下のvalidationsヘルパーを付け加える
    • レコード作成時にパスワードは空白でないこと
    • パスワード文字数は72文字以下であること
    • :password:password_confirmation両方の値が等しいこと
  • 紐づけるモデルに:password_digest属性が必要

環境

  • ruby: 2.5.0rc1
  • rails: 5.1.5
  • bcrypt: 3.1.11

やりたい事

Userモデルは下記の通り

Column Type
name string
password_digest string
  • Userクラスに:password:password_confirmation属性を加えてパスワードの検証を行えるようにしたい
  • でも検証自体はレコード作成時ではなく更新時に行いたい
  • has_secure_passwordにon: :update付けても反応してくれない

has_secure_passwordは引数を受け取ってくれる

公式ドキュメントではvalidationsの後にこのような説明が書かれている。

validationsはvalidations: falseを引数として渡すと無効化される。

つまり、下のように引数を与えればよい。


class User < ApplicationRecord
  has_secure_password validations: false

end

これによってUserクラスに:password:password_confirmation属性が付加され、validationsを行わないで済むようになった。

しかし、レコードの更新時にパスワードの値を検証したいのにこのままではどんな値も受け付けてしまう。

:passwordに紐づけられているvalidationsを引っ張り出してくる

一度メソッドのソースコードを覗いてみる事に。

rails/activemodel/lib/active_model/secure_password.rb


def has_secure_password(options = {})
  # Load bcrypt gem only when has_secure_password is used.
  # This is to avoid ActiveModel (and by extension the entire framework)
  # being dependent on a binary library.
  begin
    require "bcrypt"
  rescue LoadError
    $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
    raise
  end

  include InstanceMethodsOnActivation

  if options.fetch(:validations, true)
    include ActiveModel::Validations

    # This ensures the model has a password by checking whether the password_digest
    # is present, so that this works with both new and existing records. However,
    # when there is an error, the message is added to the password attribute instead
    # so that the error message will make sense to the end-user.
    validate do |record|
      record.errors.add(:password, :blank) unless record.password_digest.present?
    end

    validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
    validates_confirmation_of :password, allow_blank: true
  end
end

ピックアップしたいのはActiveModel::Validationsモジュールをインクルードする箇所から。:password属性に対して3つの検証を行っている。(上記のドキュメントと同じ)

  • 空白でないこと
  • 文字数は72文字以下であること
  • :password_confirmationと値を比較し、同じであること

これをこのままUserモデルに貼り付け、各validationにon: :updateを加える。


class User < ApplicationRecord
  has_secure_password validations: false

  validate(on: :update) do |record|
    record.errors.add(:password, :blank) unless record.password_digest.present?
  end

  validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED, on: :update
  validates_confirmation_of :password, allow_blank: true, on: :update
end

これでパスワード検証がレコード更新時のみ行われるようになった。:passwordと紐づけて独自のvalidationを加えることもできる。

所感

普段からAPIの使い方やエラー対策に関して公式ドキュメント、qiitaやstackoverflowに頼りきり(ほとんどがstackoverflow)だったが、ソースコードも普段から見るべきだなと感じた。


  1. passwordpassword_confirmationは仮想的な属性で外部から入力されたパスワードの検証と:password_digestの値を生成する時に使われる。データベース上のUserモデルにカラムが追加されているわけではない。値の保存は:password_digestが引き受ける。 

33
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
28