Ruby
RubyOnRails
ソースコードリーディング

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

More than 1 year has passed since last update.

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が引き受ける。