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)だったが、ソースコードも普段から見るべきだなと感じた。
-
password
とpassword_confirmation
は仮想的な属性で外部から入力されたパスワードの検証と:password_digest
の値を生成する時に使われる。データベース上のUserモデルにカラムが追加されているわけではない。値の保存は:password_digest
が引き受ける。 ↩