Railsのシンプルなログイン機能 Part2

目次

はじめに

 ログイン機能の勉強として機能を細分化しつつ実装します。

 理解するための実装なので設計上好ましくない実装が含まれます。できるだけシンプルに実装し、理解し易さを求めたので今回のような実装になりました。ないとは思いますが、後述するコードを参考にする際はお気をつけください。

  • 環境
バージョン
OS OS X 10.15.6
ruby 2.6.4
rails 6.0.3

 前回の続きをやっていきます。前回はユーザー情報をデータベースに登録する機能を実装しました。それに次の項目を加えます。

  1. パスワードを保存する際の暗号化
  2. 登録情報のバリデーション
  3. エラー処理

設計

  • データベース

 あとで実装するパスワードの暗号化のためにパスワードのカラム名をpassword_digestというカラムに変更します。

カラム 用途 データ型
id 自動生成のID intger
name ユーザーネーム string
password_digest パスワード string
  • 機能(追加)

    1. パスワードを暗号化してデータベースに保存する

    2. 登録情報(ユーザーネーム、パスワード)の登録制限機能(バリデーション)

    3. 項目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? %>とすることで、エラーがある時はエラーを表示し、エラーがないときは通常の登録フォームを表示することができます。

全て空欄でフォームを送信すると次のようになります。

f:id:aoshota:20200928170842p:plain
ユーザー登録失敗時のエラーメッセージ(全て空欄)

passwordとpassword_confirmationが違う時は次のようなエラーになります。

f:id:aoshota:20200928171038p:plain
ユーザー登録失敗時のエラーメッセージ(確認用のパスワードが間違っている時)

エラー表示は実装できましたが、今のままだとエラー表示後にブラウザを再読み込みすると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アクションに設定しています。

まとめ

今回でユーザーの登録機能が完成しました。

次回はユーザーの編集・削除機能の追加となります。

参考文献

Ruby on Rails ガイド

Ruby on Rails チュートリアル