概要
bcrypt
でhas_secure_password
を使ったときの整合性チェックの挙動はtext_field
の整合性チェックと少し挙動が違う。
has_secure_passwordをモデルに書くと、:password
に対する整合性を外してもpresence
とconfirmation
の検証が強制的に行われる。このため意図しないメッセージが重複したり、敢えて空白の入力をスルーしたりしたい場合に、この検証が邪魔になることがある。
この強制的な検証を止めたいときには、has_secure_password
のオプションにvalidations: false
を指定する(Railsドキュメント)。
手順
設定
- Gemの
bcrypt
がインストールされていて、User
モデルにhas_secure_password
が記述されている User
モデルではpassword_digest
が準備されている- フォームで
password
の入力欄と再確認用のpassword_confirmation
の入力欄の2つを表示する - フォーム送信後、2つの入力欄の値が等しいかどうかをバリデーションで確認する
ルーティング
users
コントローラーのconfirm_password
アクションで入力フォームを表示- フォームからのPOSTを
confirm_password_process
アクションで受け取って処理
1 2 3 4 5 6 |
Rails.application.routes.draw do ..... get 'users/confirm_password', as: 'confirm_password' post 'users/confirm_password_process', as: 'confirm_password_process' ..... end |
1 2 |
confirm_password GET /users/confirm_password(.:format) users#confirm_password confirm_password_process POST /users/confirm_password_process(.:format) users#confirm_password_process |
モデルの生成
:name
属性と:password_digest
属性を持つUser
モデルを構成する。
1 2 3 4 5 6 7 8 9 10 |
class CreateUsers < ActiveRecord::Migration[5.1] def change create_table :users do |t| t.string :name t.string :password_digest t.timestamps end end end |
マイグレート後
1 2 3 4 5 6 7 8 9 10 11 |
mysql> DESCRIBE users; +-----------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-----------------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | name | varchar(255) | YES | | NULL | | | password_digest | varchar(255) | YES | | NULL | | | created_at | datetime | NO | | NULL | | | updated_at | datetime | NO | | NULL | | +-----------------+--------------+------+-----+---------+----------------+ 5 rows in set (0.00 sec) |
ビュー
名前入力フィールド、パスワード入力フィールドとパスワード確認フィールドを準備する。確認フィールドは入力フィールドの名前に_confirmation
ポストフィックスを付ける(入力フィールド:password
に対して:password_confirmation
)。
1 2 3 4 5 6 7 8 9 10 |
<h1>パスワード整合性</h1> <%= form_with(model: @user, url: confirm_password_process_path, local: true) do |f| %> <%= f.text_field(:name, placeholder: "名前を入力") %> <%= f.password_field(:password, placeholder: "パスワードを入力") %> <%= f.password_field(:password_confirmation, placeholder: "パスワードを再入力") %> <%= f.submit("送信") %> <% end %> <%= link_to("Topページに戻る", top_path) %> |
モデル
Userモデルでhas_secure_password
を設定し、バリデーションではパスワードの整合性のみ検証させる。
1 2 3 4 5 |
class User < ApplicationRecord has_secure_password validates(:password, confirmation: true) end |
コントローラー
confirm_password
アクションでUser
インスタンスを準備してフォームを表示し、confirm_password_process
でフォームから受け取ったパラメーターの整合性をチェック。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class UsersController < ApplicationController def confirm_password @user = User.new end def confirm_password_process puts "----- in confirm password process ----" puts "PARAMETER:" p user_params_with_confirmation @user = User.new(user_params_with_confirmation) puts "@USER:" p @user puts "@USER.VALID?" p @user.valid? p @user.errors.messages redirect_to confirm_password_path and return end private def user_params_with_confirmation params.require(:user).permit(:name, :password, :password_confirmation) end end |
実行結果
強制的なバリデーションの実行
:password
と:password_confirmation
が"aaa"
と"aaa"
のときは、valid? == true
でエラーメッセージは空。
パラメーターにはpassword
とpassword_confirmation
の内容が平文で保持されているが、User
インスタンスにあるのはpassword_digest
のみ。
1 2 3 4 5 6 7 8 |
----- in confirm password process ---- PARAMETER: <ActionController::Parameters {"name"=>"", "password"=>"aaa", "password_confirmation"=>"aaa"} permitted: true> @USER: #<User id: nil, name: "", password_digest: "$2a$12$kHW538Pg.1ooaUjI0xx.QuGQ.LPdRzsXuXhsrr4z3mP...", created_at: nil, updated_at: nil> @USER.VALID? true {} |
:password
と:password_confirmation
の内容が異なる場合はvalid? == false
で、エラーメッセージは同じ"doesn't match ..."
が2回現れている。
:password
に何か入力して:password_confirmation
を空欄にしても同じ結果になる。
1 2 3 4 5 6 7 8 |
----- in confirm password process ---- PARAMETER: <ActionController::Parameters {"name"=>"", "password"=>"aaa", "password_confirmation"=>"bbb"} permitted: true> @USER: #<User id: nil, name: "", password_digest: "$2a$12$Tb3RGbBBeAvToWypvwmaFe5bznX2gcMGMZXic8EkqcL...", created_at: nil, updated_at: nil> @USER.VALID? false {:password_confirmation=>["doesn't match Password", "doesn't match Password"]} |
:password
を空欄にした場合、:password_confirmation
の内容の有無に関わらずvalid? == false
で、エラーメッセージは"can't be blank"
が2回、"doesn't match ..."
が1回発生。
また、:password
と:password_confirmation
の両方を空白のままにした時も同じ結果になる。
1 2 3 4 5 6 7 8 |
----- in confirm password process ---- PARAMETER: <ActionController::Parameters {"name"=>"", "password"=>"", "password_confirmation"=>"aaa"} permitted: true> @USER: #<User id: nil, name: "", password_digest: nil, created_at: nil, updated_at: nil> @USER.VALID? false {:password=>["can't be blank", "can't be blank"], :password_confirmation=>["doesn't match Password"]} |
:password
と:password_confirmation
の内容がそれぞれA、B、空白の組み合わせに対する結果をまとめると以下のようになり、検証結果が不適切な場合の全てでメッセージが2つ出されている。
pass\conf | A | B | 空白 |
A | true [] |
false “doesn’t match” “doesn’t match” |
false “doesn’t match” “doesn’t match” |
B | false “doesn’t match” “doesn’t match” |
true [] |
false “doesn’t match” “doesn’t match” |
空白 | false “can’t be blank” “doesn’t match” |
false “can’t be blank” “doesn’t match” |
false “can’t be blank” “doesn’t match” |
bcryptによるバリデーションの抑止
ここで、has_secure_password
のオプションでバリデーションを無効にしてみる。
1 2 3 4 5 |
class User < ApplicationRecord has_secure_password(validations: false) validates(:password, confirmation: true) end |
これで先と同じ票のように実行してみる。
pass\conf | A | B | 空白 |
A | true [] |
false “doesn’t match” |
false “doesn’t match” |
B | false “doesn’t match” |
true [] |
false “doesn’t match” |
空白 | false “doesn’t match” |
false “doesn’t match” |
false “doesn’t match” |
validates
の方でpresence
のチェックをせずconfirmation
だけをチェックしているので、空白の場合でも"can't be blank"
は発生していない。
また、bcrypt
側の検証を止めているので、"doesn't match ..."
のメッセージもvalidates
のconfirm
によるものだけになっている。