モデルにパスワードを実装するとき、has_securepassword
を使っています。
これを使えばpassword
, password_confirmation
の属性を作って、password_digest
属性に BCrypt でハッシュした password の値を保存してくれます。
このpassword_confirmation
という属性、実は値をnil
にすると validation を通ってしまいます。
今回は、has_secure_password
の挙動について記事にします。
has_secure_password とは
has_secure_password
とは、Rails が標準に提供するメソッドです。
これを使うことで、簡単にモデルにパスワードを暗号化して保存、認証する機能を追加できます。
Railsチュートリアルで使ったことがある方も多いのではないでしょうか。
validationをコンソールで確認する
実際にpassword_confirmation
の値にnil
を入れるとどうなるのか、コンソールで確認します。
終了時にデータが元に戻るように--sandbox
オプションをつけています。
$ bin/rails c --sandbox
Loading development environment in sandbox (Rails 6.0.0)
Any modifications you make will be rolled back on exit
モデルをnew
で作成し、valid?
メソッドで validation を確認していきます。
irb(main):001:0> user = User.new(name: 'test', password: 'password', password_confirmation: 'password')
(0.2ms) BEGIN
=> #<User id: nil, name: "test", password_digest: [FILTERED], created_at: nil, updated_at: nil>
irb(main):002:0> user.valid?
=> true # passwordとpassword_confirmationの値が一致しているためtrue
irb(main):003:0> user.password_confirmation = 'badpassword'
=> "badpassword" # password_confirmationの値を変更する
irb(main):004:0> user.valid?
=> false # passwordとpassword_confirmationの値が一致していないためfalse
irb(main):005:0> user.password_confirmation = nil
=> nil # password_confirmationの値をnilに変更する
irb(main):006:0> user.valid?
=> true # validationが通る!
このようにpassword_confirmation
がnil
の時は validation が通ってしまいます。
値が nil の時の挙動
ActiveModel::SecurePassword::ClassMethodsによると、
If confirmation validation is not needed,
simply leave out the value for XXX_confirmation (i.e. don't provide a form field for it).
When this attribute has a nil value, the validation will not be triggered.
つまり、**password_confirmationの値がnilのとき、password_confirmationは使われない
**ようです。
先ほどの例だと、password_confirmation
がnil
の時はpassword_confirmation
を無視していたため、 validation が通ったわけです。
password_confirmation 部分の実装
has_secure_password
の実装も確認してみましょう。
上記のコードの全体像から、has_secure_password
メソッドの実装部分を切り取りました。
def has_secure_password(attribute = :password, validations: true)
# 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.new(attribute)
if validations
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(attribute, :blank) unless record.send("#{attribute}_digest").present?
end
validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
validates_confirmation_of attribute, allow_blank: true
end
end
このコード内の
validates_confirmation_of attribute, allow_blank: true
というコードよってpassword_confirmation
属性が追加されています。
また、このコードにはallow_blank
オプションが付いています。
Rails ガイドによると、
:allow_blank オプションは:allow_nil オプションと似ています。
このオプションを指定すると、属性の値が blank?に該当する場合(nil や空文字など)にバリデーションがパスします。
つまり、
password_confirmation.blank? == true
だと validation をスキップするように実装されています。
まとめ
確認用の属性を使わないように設定することが容易に実装できるようです。
パスワードの validation 周りが柔軟に実装できそうでとても良いですね。
何か直すべき点、間違った記述があればコメント等で指摘していただければと思います。
以下Tips
##has_secure_password の新機能
Rails6.0 からhas_secure_password
にカラムを指定できるようになりました。
先ほどの実装のメソッドの宣言部分を見てみると、
def has_secure_password(attribute = :password, validations: true)
と、attribute
にシンボルで引数を渡すことで様々なカラムでhas_secure_password
を使用出来るようになりました。
これでactivate_digest
, reset_digest
なんかも実装しやすくなったのではないでしょうか。
詳しくは@suketa 様の
Rails6 のちょい足しな新機能を試す 11(has_secure_password 編)
という記事が非常に分かりやすいです。是非ご覧ください。
参考文献
https://naokirin.hatenablog.com/entry/2019/03/29/032801
https://railsguides.jp/6_0_release_notes.html
https://railsguides.jp/active_record_validations.html