7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Ruby on Rails] パスワード検証における allow_nil?の挙動

Last updated at Posted at 2023-05-19

疑問:空文字列がnil判定される?

会社の研修で Rails Tutorial をやっていたとき、第10章「ユーザーの更新」のところでパスワードの入力欄が空の場合でもエラーを出さないようにする、という項目がありました

Tutorialの説明によると User Model のバリデーションのところに allow_nil? をつけることによって例外処理を加える、とあります

password 属性のバリデーションに例外として allow_nil? を加えることによって条件を満たしていない場合でも値が nil の場合には例外的にバリデーションを通過させるようにする、という記述です

しかし User Controller に debugger を埋め込んで実際にフォームに渡っているデータを確認すると

(ruby) user_params
<ActionController::Parameters {"name"=>"Rails Tutorial", "email"=>"example@railstutorial.com", "password"=>"", "password_confirmation"=>""} permitted: true>

ということで password は nil ではなく空文字列("")が入っていました

ruby の仕様では空文字列は nil とは判定されません

3.1.2 :001 > "".nil?
 => false 

allow_nil?はあくまでnilのみを例外として扱い、空文字列や空白文字は通常のバリデーション処理が行われるため、一見するとこのままでは例外処理がうまく動かなさそうです

答え:has_secure password の仕様

しかし実際には User model で記述されている has_secure_password の仕様によって、空文字列("")を代入した場合はユーザーの password 属性に値が代入されないようになっています

このことを簡単な実験によって確かめてみましょう

まず適当なユーザーインスタンスを作成し name 属性に空文字列を代入してみます

3.1.2 :002 > user = User.first
 => "" 
3.1.2 :003 > user.name = ""
 => "" 
3.1.2 :004 > user.name
 => ""

想定通り user.name には空文字列("")が代入されています

今度は password 属性に空文字列を代入してみましょう


3.1.2 :005 > user.password = ""
 => "" 
3.1.2 :006 > user.password
 => nil 

なんと空文字列を代入したはずなのに実際に user.password に代入されている値は nil となっています

どうしてこんなことが起こるのか、has_secure_password のソースコードを見て挙動を確認してみます

該当箇所は118-127行目のInstanceMethodActivationクラスの中にある以下の記述です

define_method("#{attribute}=") do |unencrypted_password|
  if unencrypted_password.nil?
    instance_variable_set("@#{attribute}", nil)
    self.public_send("#{attribute}_digest=", nil)
  elsif !unencrypted_password.empty?
    instance_variable_set("@#{attribute}", unencrypted_password)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
    self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
  end
end

この部分はセッターと呼ばれる部分でインスタンス変数の password 属性への値の代入が定義されています

このメソッドで unencrypted_password が空文字列("")である場合を考えてみましょう

まず空文字列は nil ではないので一番目の条件式は false となります

続いて二番目の条件式も "".empty?が true であることを考えると、その否定なので条件式全体としては false となります

ここで password 属性への代入は instance_variable_set メソッドで行われていることを考慮すると、値が空文字列の場合このメソッドでは何も実行されないことになります

結果的に password 属性に代入した空文字列("")はモデルのインスタンス変数としては nil が代入されるわけです

allow_nil?が評価されるタイミングはフォームのパラメータがPOSTされるタイミングではなくモデルのインスタンスが save されるタイミングですので、今回password属性のバリデーションとして評価されるのは空文字列ではなく nil となり、無事バリデーションを通過することができるのでした

ひとこと

今回のようなポイントは何気なくチュートリアルを進めてしまうと疑問に思わずに通り抜けてしまう人も多いと思います

しかしよくよく考えてみると空文字列が渡っているはずの password で allow_nil? が成立するのは不思議です

初心者向けのTutorialでも、丁寧に読んでいくと、最終的には rails のソースコードを見なければわからないようなトピックが隠れているのは面白いと思いました

本記事で書いた現象について一緒に調査してくれた会社の同期3人に感謝します

7
3
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
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?