概要
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... |