Railsのシンプルなログイン機能 Part2
目次
はじめに
ログイン機能の勉強として機能を細分化しつつ実装します。
理解するための実装なので設計上好ましくない実装が含まれます。できるだけシンプルに実装し、理解し易さを求めたので今回のような実装になりました。ないとは思いますが、後述するコードを参考にする際はお気をつけください。
- 環境
バージョン | |
---|---|
OS | OS X 10.15.6 |
ruby | 2.6.4 |
rails | 6.0.3 |
前回の続きをやっていきます。前回はユーザー情報をデータベースに登録する機能を実装しました。それに次の項目を加えます。
- パスワードを保存する際の暗号化
- 登録情報のバリデーション
- エラー処理
設計
- データベース
あとで実装するパスワードの暗号化のためにパスワードのカラム名をpassword_digestというカラムに変更します。
カラム | 用途 | データ型 |
---|---|---|
id | 自動生成のID | intger |
name | ユーザーネーム | string |
password_digest | パスワード | string |
機能(追加)
パスワードを暗号化してデータベースに保存する
登録情報(ユーザーネーム、パスワード)の登録制限機能(バリデーション)
項目2で設定した入力制限により登録ができない時の処理(エラー処理)
ビュー
ファイル名 | 内容 |
---|---|
index.html.erb | ユーザー一覧 |
new.html.erb | 登録画面 |
実装
パスワードを暗号化してデータベースに保存する
パスワードの暗号化機能に必要なことは大きく分けると以下の通りです。
・has_secure_passwordメソッドを使う
・パスワードはDBのpassword_digestカラムに保存する
・ bcrypt gemの追加
- データベースの変更
パスワードを保存するカラム名をpassword_digestに変更します。
次のコマンドでマイグレーションファイルを作成します。
$ rails g migration rename_password_password_digest_column_to_Users
作成したマイグレーションファイルを次のように変更します。
class RenamePasswordPasswordDigestColumnToUsers < ActiveRecord::Migration[6.0] def change rename_column :users, :password, :password_digest end end
最後にdb:migtateを実行しDBに反映させます。
$ rails db:migrate
Userモデルを確認すると、annotateによるスキーマ情報が変更されているのがわかります(実際にDBも変更されています)。
# == Schema Information # # Table name: users # # id :integer not null, primary key # name :string # password_digest :string # created_at :datetime not null # updated_at :datetime not null # class User < ApplicationRecord end
- Userコントローラーの編集
has_secure_passwordではpassword属性とpassword_confirmation属性のものがフォームから送られてきた時、その2つが同じ値ならばパスワードを暗号化してDBのpassword_digestカラムに暗号化されたパスワードを保存します。そのため、strong parametersに:password_cofirmationを追加します。
class UsersController < ApplicationController def index @users = User.all end def new end def home end def create @user = User.new(user_params) @user.save redirect_to '/users/index' end private def user_params params.require(:user).permit(:name, :password, :password_cofirmation) end end
- viewファイルの編集
Userコントローラーと同様にpassword_cofirmation属性のinputタグを追加します。
<h1>ユーザー登録</h1> <%= link_to 'ユーザー一覧', '/users/index' %> <form action="/users/create" method="post"> <%= hidden_field_tag :authenticity_token, form_authenticity_token %> <ul> <li> <label for="user_name">Name:</label> <input type="text" id="user_name" name="user[name]"> </li> <li> <label for="user_password">Password:</label> <input type="password" id="user_password" name="user[password]"> </li> <li> <label for="user_password">Password(confirm):</label> <input type="password" id="user_password_confirmation" name="user[password_confirmation]"> </li> <li> <input type="submit" value="登録"> </li> </ul> </form>
- bcryptの追加
Gemfileにbcryptを追加してbundle installを実行します。
bcryptはデフォルトでコメントアウトされている場合があるのでコメントアウト されていたらコメントアウトを解除し、なければ追加します。
... gem 'bcrypt', '3.1.7' ...
$ bundle install
- has_secure_passwordの追加
userモデルにhas_secure_passwordを追加します。
# == Schema Information ... class User < ApplicationRecord has_secure_password end
以上で、パスワードの暗号化が実装されました。暗号化する前と後のデータベースの中身は次の通りです。
SQLite version 3.28.0 2019-04-15 14:49:49 Enter ".help" for usage hints. sqlite> SELECT "users".* FROM "users"; ... 8|foo|bar|2020-09-28 06:06:23.205372|2020-09-28 06:06:23.205372 9|hoge|$2a$12$deW7iUy7UnXXUMAYPLur.OHd7G7lhzTbI0AwA95I7XO.QOBZjNe4e|2020-09-28 06:17:23.433464|2020-09-28 06:17:23.433464 sqlite>
一番左のカラムがidであり、idが9のデータを挿入する前に暗号化を追加しました。左から3番目のカラムがpassword_digestのカラムで暗号化されているのがわかります。
登録情報(ユーザーネーム、パスワード)の登録制限機能(バリデーション)
ここでは、名前やパスワードの登録に制限をかけます。具体的には次の通りです。
・名前:50文字以内。空欄禁止。
・パスワード:8文字以上。空欄禁止。
パスワードに関して、8文字以上の制限だけで空欄禁止も含むかと思われますが、文字数制限だけでは6文字分の空白スペースなどに対応できないため空白禁止の制限も追加します。
この制限を実装するにはUserモデルにバリデーション機能を追加します。
- Usesrモデルの編集
Userモデルに次のようにバリデーションを追加します。
# == Schema Information ... class User < ApplicationRecord has_secure_password validates :name, presence: true, length: { maximum: 50 } validates :password, presence: true, length: { minimum: 8 } end
これにより、名前は1文字以上50文字以内(空欄禁止)でパスワードは8文字以上という制限が追加されました。
登録制限は実装できましたが、このままでは登録できなかった時と登録できた時の違いがわかりません。そこで、次の項で登録失敗の時はエラーを表示するようにします。
登録ができない時の処理(エラー処理)
エラー処理の手順は次の通りです。
①Userコントローラーに登録成功時と失敗時の分岐を追加する。
②Userコントローラーのnewメソッドにインスタンス変数を追加する。
③viewファイルにエラーメッセージの表示を追加
- Userコントローラーの変更
次のようにUserコントローラーを修正します。
class UsersController < ApplicationController def index @users = User.all end def new @user = User.new end def home end def create @user = User.new(user_params) if @user.save redirect_to '/users/index' else render '/users/new' end end private def user_params params.require(:user).permit(:name, :password, :password_cofirmation) end end
newメソッドでは空のインスタンス変数を作成しています。これがないと登録ページにアクセスした時、インスタンス変数@Userがnilになりエラーが出ます。
登録成功時と失敗時の分岐はcreateメソッドのif分で実装しています。@user.saveはDBへの保存は成功した時trueを返し、失敗した時(登録制限に引っ掛かった時)falseを返します。成功時は今まで通りユーザー一覧にリダイレクトし、失敗時は登録ページ(現在の)にリダイレクトしエラーを表示します。
- viewファイルの変更
viewファイルを次のように修正します。
<h1>ユーザー登録</h1> <%= link_to 'ユーザー一覧', '/users/index' %> <form action="/users/create" method="post"> <%= hidden_field_tag :authenticity_token, form_authenticity_token %> <% if @user.errors.any? %> <div id="error_explanation"> <div> The form contains <%= pluralize(@user.errors.count, "error") %>. </div> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <ul> <li> <label for="user_name">Name:</label> <input type="text" id="user_name" name="user[name]"> </li> <li> <label for="user_password">Password:</label> <input type="password" id="user_password" name="user[password]"> </li> <li> <label for="user_password">Password(confirm):</label> <input type="password" id="user_password_confirmation" name="user[password_confirmation]"> </li> <li> <input type="submit" value="登録"> </li> </ul> </form>
登録に失敗した時は@userにエラーのオブジェクトが追加されます。<% if @user.errors.any? %>とすることで、エラーがある時はエラーを表示し、エラーがないときは通常の登録フォームを表示することができます。
全て空欄でフォームを送信すると次のようになります。
passwordとpassword_confirmationが違う時は次のようなエラーになります。
エラー表示は実装できましたが、今のままだとエラー表示後にブラウザを再読み込みするとRailsのエラーになってしまいます。そこでルーティングを次のように変更します。
Rails.application.routes.draw do get 'users/index' get 'users/new' get '/users/create', to: 'users#new' post 'users/create' root 'users#index' end
フォームの送信はPOSTリクエストで送信されるようにHTMLを作成したがブラウザの読み込みはGETリクエストなのでcreateアクションにGETリクエストが来た時のルーティングをnewアクションに設定しています。
まとめ
今回でユーザーの登録機能が完成しました。
次回はユーザーの編集・削除機能の追加となります。