概要
Gemの1つ、bcryptをインストールすることで、パスワードの暗号化が容易に実装できるようになる。手順の概要は、Userモデルを例にとると以下のようになる。
<ユーザー登録時>
- bcryptを導入する
- usersテーブルのパスワードカラムを- password_digestとする
- Userモデルに- has_secure_passwordメソッドを記述
- サインインフォームに:passwordフィールドを置く
- フォームからのPOSTに対してUserインスタンスを生成
- Userインスタンスをデータベースに保存
<ユーザー認証時>
- フォームのPOSTに対してユーザーを特定し、Userインスタンスを生成
- フォームに入力されたパスワードパラメーターを使って、Userインスタンスのauthenticateメソッドで認証
手順
設定
- 登録画面でユーザーの名前とパスワードを登録
- 認証画面でユーザーの名前とパスワードを入力して認証
bcryptの導入
Gemfile中bcryptのコメントを外す。なければ追加。
| 1 2 | # Use ActiveModel has_secure_password gem 'bcrypt', '~> 3.1.7' | 
Railsサーバー停止状態でインストール。
| 1 | $ bundle install | 
ユーザーモデルの準備
マイグレーションファイルのパスワード属性はpassword_digestとする。
| 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) | 
ルーティング
この例では4つのアクションを以下のようにルーティング
- secure_sign_upで登録フォームを表示
- secure_sign_up_processで登録処理(パスワードを暗号化)
- secure_sign_inで認証フォームを表示
- secure_sign_in_processで認証処理
| 1 2 3 4 5 6 7 8 | Rails.application.routes.draw do   .....   get   'users/secure_sign_up', as: 'secure_sign_up'   post  'users/secure_sign_up_process', as: 'secure_sign_up_process'   get   'users/secure_sign_in', as: 'secure_sign_in'   post  'users/secure_sign_in_process', as: 'secure_sign_in_process'   ..... end | 
| 1 2 3 4 |         secure_sign_up GET  /users/secure_sign_up(.:format)              users#secure_sign_up secure_sign_up_process POST /users/secure_sign_up_process(.:format)      users#secure_sign_up_process         secure_sign_in GET  /users/secure_sign_in(.:format)              users#secure_sign_in secure_sign_in_process POST /users/secure_sign_in_process(.:format)      users#secure_sign_in_process | 
ユーザー登録
登録画面
登録フォームの基本構成はシンプルで、パスワードは:passwordの名前でpassword_formを置くだけでよい。
| 1 2 3 4 5 6 7 8 9 | <h1>パスワード暗号化~サインアップ</h1> <%= form_with(model: @user, url: secure_sign_up_process_path, local: true) do |f| %>   <%= f.text_field(:name, placeholder: "名前を入力") %>   <%= f.password_field(:password, placeholder: "パスワードを入力") %>   <%= f.submit("送信") %> <% end %> <%= link_to("Topページに戻る", top_path) %> | 
登録処理
フォームからのPOSTに対して、:passwordを含むパラメーターでUserインスタンスを生成し、データベースに保存するだけでパスワードが暗号化される。
| 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 secure_sign_up     @user = User.new   end   def secure_sign_up_process     puts "----- in sign up process -----"     puts "USER_PARAMS:"     p user_params     @user = User.new(user_params)     puts "@USER:"     p @user     @user.save     redirect_to secure_sign_in_path and return   end   .....   private   def user_params     params.require(:user).permit(:name, :password)   end end | 
ユーザー認証
認証画面
認証画面も登録画面と同じで:passwardを拾えばよい。
| 1 2 3 4 5 6 7 8 9 | <h1>パスワード暗号化~サインイン</h1> <%= form_with(model: @user, url: secure_sign_in_process_path, local: true) do |f| %>   <%= f.text_field(:name, placeholder: "名前を入力") %>   <%= f.password_field(:password, placeholder: "パスワードを入力") %>   <%= f.submit("送信") %> <% end %> <%= link_to("Topページに戻る", top_path) %> | 
認証処理
認証手順は以下の通り。
- ユーザー名などからデータベース上のユーザーを取り出してUserインスタンスを生成
- 生成したユーザーインスタンスのauthenticateメソッドにフォームパラメーターの:pasuwordの内容を与えて認証
認証の際に、@user&.authenticate...と「ぼっち演算子」を使っているのは、未登録ユーザーの場合にauthenticateメソッドがエラーとなるのを防ぐため。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class UsersController < ApplicationController   .....   def secure_sign_in     @user = User.new   end   def secure_sign_in_process     puts "----- in sign in process -----"     puts "USER_PARAMS:"     p user_params     @user = User.find_by(name: user_params[:name])     puts "@USER:"     p @user     puts "AUTHENTICATE"     p @user&.authenticate(user_params[:password])     if @user&.authenticate(user_params[:password])       puts "Successfully signed in!"     else       puts "Sign in failed..."     end     redirect_to secure_sign_in_path and return   end   private   def user_params     params.require(:user).permit(:name, :password)   end end | 
実行結果
登録時
名前:山田、パスワード:yamadaとして登録した結果が以下の通り。
パラメーターで平文のパスワードが、インスタンス生成後にはダイジェストで保持されていて、この値がデータベースに保存される。
| 1 2 3 4 5 | ----- in sign up process ----- USER_PARAMS: <ActionController::Parameters {"name"=>"山田", "password"=>"yamada"} permitted: true> @USER: #<User id: nil, name: "山田", password_digest: "$2a$12$x.vfAVdh6k8aaZdVHI6CreLHas8rzd316xNx/yW9Yx6...", created_at: nil, updated_at: nil> | 
認証時
名前:山田、パスワード:yamadaとしてサインインした場合。
フォーム入力から生成されたUserインスタンスのpassword_digestが上の内容と等しくなっているのが確認できる。
また、authenticateの結果が正しい場合は、Userインスタンスが返されている。
| 1 2 3 4 5 6 7 8 9 | ----- in sign in process ----- USER_PARAMS: <ActionController::Parameters {"name"=>"山田", "password"=>"yamada"} permitted: true>   User Load (0.4ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = '山田' LIMIT 1 @USER: #<User id: 1, name: "山田", password_digest: "$2a$12$x.vfAVdh6k8aaZdVHI6CreLHas8rzd316xNx/yW9Yx6...", created_at: "2021-04-10 05:58:07", updated_at: "2021-04-10 05:58:07"> AUTHENTICATE #<User id: 1, name: "山田", password_digest: "$2a$12$x.vfAVdh6k8aaZdVHI6CreLHas8rzd316xNx/yW9Yx6...", created_at: "2021-04-10 05:58:07", updated_at: "2021-04-10 05:58:07"> Successfully signed in! | 
異なるパスワードを入力した場合。
password_digestの値が異なっていて、authenticateの戻り値はfalse。
| 1 2 3 4 5 6 7 8 9 | ----- in sign in process ----- USER_PARAMS: <ActionController::Parameters {"name"=>"山田", "password"=>"aaa"} permitted: true>   User Load (0.4ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = '山田' LIMIT 1 @USER: #<User id: 1, name: "山田", password_digest: "$2a$12$x.vfAVdh6k8aaZdVHI6CreLHas8rzd316xNx/yW9Yx6...", created_at: "2021-04-10 05:58:07", updated_at: "2021-04-10 05:58:07"> AUTHENTICATE false Sign in failed... | 
存在しないユーザーを入力した場合。
| 1 2 3 4 5 6 7 8 9 | ----- in sign in process ----- USER_PARAMS: <ActionController::Parameters {"name"=>"伊藤", "password"=>"itou"} permitted: true>   User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = '伊藤' LIMIT 1 @USER: nil AUTHENTICATE nil Sign in failed... |