概要
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アクションで受け取って処理
|
|
Rails.application.routes.draw do ..... get 'users/confirm_password', as: 'confirm_password' post 'users/confirm_password_process', as: 'confirm_password_process' ..... end |
|
|
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モデルを構成する。
|
|
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 |
マイグレート後
|
|
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)。
|
|
<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を設定し、バリデーションではパスワードの整合性のみ検証させる。
|
|
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のみ。
|
|
----- 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を空欄にしても同じ結果になる。
|
|
----- 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の両方を空白のままにした時も同じ結果になる。
|
|
----- 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のオプションでバリデーションを無効にしてみる。
|
|
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によるものだけになっている。