【Rails】IDとパスワードによる認証機能の実装【初学者】

2022年6月16日 18:21

はじめに

大抵のWebアプリでは、ユーザー登録/解除、ログイン/ログアウトといった認証機能を持っています。Railsには簡単に認証機能を導入できる「device」というGemが用意されています。「devise」は多くのRailsアプリで使われている実績のあるGemですが、「devise」を使わなくても比較的簡単に認証機能を実装できてしまいます。また、「devise」は処理のほとんどがブラックボックス化されており、初めて認証機能を実装しようという場合には何をしているのか理解しにくいといったデメリットがあります。認証機能に関する理解を深めるためにも、一度は自分で認証機能を実装してみることをおすすめします。

本記事では、IDとパスワードによる認証機能を実装する方法について説明します。

ユーザー登録/解除の実装

モデルの作成

テーブルの作成

まず、ユーザー情報を保持しておくためのユーザーテーブルを作成します。今回は、ユーザー登録/解除、ログイン/ログアウトを行うためだけのシンプルな構成とします。

以下のコマンドを実行して、ユーザーモデルを作成します。

$ rails generate model User email:string password_digest:string

モデルを作成するときは、モデル名を単数形(今回はUser)にします。コントローラーを作成するときは複数形なので、混同しないように注意してください。

モデル名の後に、追加するカラム名とそのデータ型を列挙します。ID(メールアドレス)とパスワードのみのシンプルな構成です。パスワードのカラム名は必ずXXXXX_digestとしてください(理由については後述)。

インデックスの作成

次に、作成するユーザーテーブルにインデックスを追加します。テーブルにインデックスを追加することで、レコードの探索が高速になり、またレコードの一意性を保つことができるようになります。

以下のコマンドを実行して、マイグレーションファイルを作成します。

$ rails generate migration AddIndexToUsersEmail

作成されたマイグレーションファイルに以下を追記します。

YYYYMMDDHHMMSS_add_index_to_users_email.rb

class AddIndexToUsersEmail < ActiveRecord::Migration[6.1]
  def change
    # 以下を追記
    add_index :users, :email, unique: true
  end
end

マイグレーションファイルの作成・編集については以下の記事を参照してください。

マイグレーションの実行

必要なマイグレーションファイルが揃ったのでマイグレーションを実行します。

$ rails db:migrate

マイグレーションの実行については以下の記事を参照してください。

モデルの編集

作成したばかりのユーザーモデルでは、IDとなるメールアドレスにどんな値でも設定できてしまったり、パスワードが暗号化されずに設定されてしまったりと、不適切な構成となっているので、適切な構成となるように編集していきます。

作成したユーザーモデルファイルに以下を追記します。

user.rb

class User < ApplicationRecord
  # 以下を追記
  before_save { self.email = email.downcase }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  validates :password, presence: true, length: { minimum: 6 }
  has_secure_password
end

ここでは大きく分けて以下の2つのことを行っています。

対象のカラム 説明
email 「必須」「最大255文字」「正しいフォーマット」「一意」を検証。保存する前に大文字を小文字に変換。
password 「必須」「最小6文字」を検証。セキュアなパスワードを設定。

メールアドレス

メールアドレスに関する設定は以下の部分です。

  before_save { self.email = email.downcase }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }

3〜5行目でメールアドレスに関するバリデーションを設定しています。4行目のフォーマットは、2行目で設定している正規表現を使って検証されます。1行目のbefore_actionは、レコードが保存される前の動作を設定でき、ここでは大文字を小文字に変換するようにしています。

パスワード

パスワードに関する設定は以下の部分です。

  validates :password, presence: true, length: { minimum: 6 }
  has_secure_password

1行目でパスワードに関するバリデーションを設定しています。

2行目のhas_secure_passwordというヘルパーメソッドは、記述するだけでセキュアなパスワードを扱えるようになる特殊なメソッドです。このヘルパーメソッドを使うと、passwordpassword_confirmationという疑似属性が使えるようになります。この2つの疑似属性は一致していることを検証するバリデーションが設定されています。そのため、password_confirmationに関するバリデーションを設定する必要はありません。

また、パスワードの値はXXXXX_digestというカラムに保存されるため、あらかじめ対象テーブルに同名のカラムを追加しておく必要があります(今回は「テーブルの作成」セクションで作成済み)。

has_secure_passwordはパスワードを暗号化してデータベースに保存します。暗号化には「bcrypt」というGemをインストールする必要があります。「bcrypt」はGemfileの23行目あたりにコメントアウトされているので、コメントアウトを外してからbundle installを実行します。

Gemfile

# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7'

コントローラーの作成

ユーザー登録/解除処理のコントローラーを作成します。

$ rails generate Controller Users

作成したコントローラーに以下を追記します。

users_controller.rb

class UsersController < ApplicationController
  # 以下を追記
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to profile_path(@user)
    else
      render 'new'
    end
  end

  def show
    @user = User.find(params[:id])
  end

  def destroy
    user = User.find(params[:id])
    user.destroy
    redirect_to signup_path
  end

  private
    def user_params
      params.require(:user).permit(:name, :email, :password, :password_confirmation)
    end
end

一般的なCRUDの各処理を記述します。各アクションは必要最低限の処理しか行っていないため、必要に応じてフラッシュメッセージの設定やユーザー情報の変更などの処理を追加してください。

ビューの作成

ユーザー登録画面

ユーザー登録画面を作成します。app/views/users/ディレクトリ配下にnew.html.erbファイルを作成し、作成したファイルに以下を記述します。

new.html.erb

<h1>ユーザー登録</h1>

<%= form_with model: @user, url: signup_path, local: true do |form| %>
  <% if @user.errors.any? %>
    <strong><%= @user.errors.count %>個のエラーがあります。</strong>
    <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  <% end %>

  <%= form.label :email, 'メールアドレス' %>
  <%= form.email_field :email %>

  <%= form.label :password, 'パスワード' %>
  <%= form.password_field :password %>

  <%= form.label :password_confirmation, 'パスワード(確認)' %>
  <%= form.password_field :password_confirmation %>

  <%= form.submit '登録' %>
<% end %>

ユーザー登録画面では、メールアドレス、パスワード、パスワード(確認)を入力するテキストボックス、送信ボタンを配置します。フォームの書き方については以下の記事を参照してください。

ユーザー情報画面

続いて、ユーザー情報画面を作成します。app/views/users/ディレクトリ配下にshow.html.erbファイルを作成し、作成したファイルに以下を記述します。

show.html.erb

<h1>ユーザー情報</h1>

<p>メールアドレス:<%= @user.email %></p>
<p><%= link_to '登録解除', unsubscribe_path(@user), method: :delete, data: { confirm: '本当に登録解除しますか?' } %></p>

ユーザーの登録解除を行うリンクを配置します。

ルーティングの設定

最後に、ユーザー登録/解除のルーティングを設定します。routes.rbに以下を追記します。

routes.rb

Rails.application.routes.draw do
  # 以下を追記
  get '/signup', to: 'users#new'
  post '/signup', to: 'users#create'
  get '/users/:id', to: 'users#show', as: 'profile'
  delete '/users/:id', to: 'users#destroy', as: 'unsubscribe'
end

resourcesキーワードを使ってルーティングを設定するとパスの変更ができません。ユーザー登録画面のパスは/signupにしたいので、あえて個別にルーティングを設定しています。

ユーザー登録/解除の動作確認

ここまでの実装を確認します。テストサーバーを起動し、http://localhost:3000/signupにアクセスします。

ユーザー登録フォームに値を入力し送信ボタンをクリックすると、ユーザー登録が完了しユーザー情報画面に遷移することを確認します。また、不適切な値を入力してから送信ボタンをクリックするとエラーが表示されることも確認します。ユーザー情報画面では、入力したメールアドレスが表示されており、「登録解除」リンクをクリックするとユーザー情報が削除されることを確認します。

ユーザー情報のデータベース登録状況も確認しておきます。

$ rails console
> User.find(1)
=> <User id: 1, email: "test@example.com", password_digest: [FILTERED], created_at: "2021-11-29 14:38:21.222271000 +0000", updated_at: "2021-11-29 14:38:21.222271000 +0000">

登録したユーザーの情報が設定されています。パスワードはpassword_digestカラムに暗号化されて保存されていることがわかります。

ログイン/ログアウトの実装

コントローラーの作成

ログイン/ログアウトのセッション管理を行うコントローラーを作成します。

$ rails generate controller Sessions

作成したコントローラーに以下を追記します。

sessions_controller.rb

class SessionsController < ApplicationController
  # 以下を追記
  before_action :require_login, only: [:destroy]

  def new
  end

  def create
    user = User.find_by(email: params[:email].downcase)
    if user && user.authenticate(params[:password])
      log_in(user)
      redirect_to profile_path(user)
    else
      flash.now[:danger] = 'メールアドレスかパスワードが間違っています。'
      render 'new'
    end
  end

  def destroy
    log_out
    redirect_to login_path
  end
end

createアクション内のauthenticateメソッドは、「モデルの作成」セクションでhas_secure_passwordメソッドを呼び出すことにより使えるようになるメソッドです。引数にパスワードを渡すことで認証を行い、認証結果がOKの場合は認証に使用したオブジェクトが返され、認証結果がNGの場合はfalseが返されます。

require_loginlog_inlog_outといったヘルパーメソッドは後述する「ヘルパーの作成」セクションで作成します。

ヘルパーの作成

セッションに関するヘルパーメソッドを作成します。ヘルパーはコントローラーを作成すると自動で作成されます。作成したヘルパーに以下を追記します。

sessions_helper.rb

module SessionsHelper
  # 以下を追記
  def log_in(user)
    session[:user_id] = user.id
  end

  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

  def logged_in?
    !current_user.nil?
  end

  def require_login
    redirect_to login_path if !logged_in?
  end

  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

これらのヘルパーメソッドをすべてのコントローラーで使えるようにするために、すべてのコントローラーが継承しているアプリケーションコントローラーにヘルパーを組み込みます。

applicatioin_controller.rb

class ApplicationController < ActionController::Base
  # 以下を追記
  include SessionsHelper
end

作成するヘルパーメソッドの詳細は以下の通りです。

log_in

  def log_in(user)
    session[:user_id] = user.id
  end

セッションにユーザーIDを保存します。セッションはブラウザを閉じるか明示的に削除するまでデータを保持するメモリ領域の一種です。

current_user

  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

log_inで保存したIDをキーにユーザーを検索します。一般的にデータベースへのアクセスは処理遅延につながるため、データベースへのアクセスは最小限に抑える必要があります。既に@current_userに値が存在する場合はそれを使い、存在しない場合のみデータベースにアクセスしてデータを検索します。A ||= Bという書き方はA = A || Bの短縮形で、Rubyでよく使う書き方のひとつです。

logged_in?

  def logged_in?
    !current_user.nil?
  end

ユーザーがログインしているかどうかを真偽値で返します。

メソッド末尾の?は、メソッドの戻り値が真偽値であることを示すためにつける記号です(慣用的なものであり必須ではない)。余談ですが、メソッド末尾の!は、メソッドが破壊的な処理を行うことを示すためにつける記号です。

require_login

  def require_login
    redirect_to login_path if !logged_in?
  end

ユーザーがログインしていなければログイン画面に遷移します。

log_out

  def log_out
    session.delete(:user_id)
    @current_user = nil
  end

セッションに保存されているユーザーIDを破棄し、ユーザー情報を保存している変数を初期化します。

ビューの作成

ログイン画面を作成します。app/views/sessions/ディレクトリ配下にnew.html.erbを作成し、作成したファイルに以下を記述します。

new.html.erb

<h1>ログイン</h1>

<%= form_with model: @session, local: true do |form| %>
  <%= flash[:danger] %>

  <%= form.label :email, 'メールアドレス' %>
  <%= form.email_field :email %>

  <%= form.label :password, 'パスワード' %>
  <%= form.password_field :password %>

  <%= form.submit 'ログイン' %>
<% end %>

ビューの編集

ユーザー情報画面に「ログアウト」リンクを追加します。

show.html.erb

<h1>ユーザー情報</h1>

<p>メールアドレス:<%= @user.email %></p>
<%# 以下を追記 %>
<p><%= link_to 'ログアウト', logout_path, method: :delete %></p>
<p><%= link_to '登録解除', unsubscribe_path(@user), method: :delete, data: { confirm: '本当に登録解除しますか?' } %></p>

ルーティングの設定

最後に、ログイン/ログアウトのルーティングを設定します。routes.rbに以下を追記します。

routes.rb

Rails.application.routes.draw do
  # 以下を追記
  get '/login', to: 'sessions#new'
  post '/login', to: 'sessions#create'
  delete '/logout', to: 'sessions#destroy'
end

ログイン/ログアウトの動作確認

ここまでの実装を確認します。テストサーバーを起動し、http://localhost:3000/loginにアクセスします。

ログインフォームに値を入力し送信ボタンをクリックすると、ログインが完了しユーザー情報画面に遷移することを確認します。また、不適切な値を入力してから送信ボタンをクリックするとエラーが表示されることも確認します。ユーザー情報画面では、「ログアウト」リンクをクリックするとログアウトが完了しログイン画面に遷移することを確認します。

リファクタリング

最後に、ユーザー登録/解除に関する処理のリファクタリングを行います。ヘルパーメソッドを作成したことにより、リクエストURLを使わなくてもログインしているユーザー情報が取得できるようになりました。また、ユーザー登録を行ったときにログイン処理を追加する必要があります。

ルーティングのリファクタリング

ユーザー情報画面および登録解除のルーティングを変更します。routes.rbを以下のように修正します。

routes.rb

Rails.application.routes.draw do
  get '/signup', to: 'users#new'
  post '/signup', to: 'users#create'
  # 以下の2行を修正
  get '/profile', to: 'users#show'
  delete '/unsubscribe', to: 'users#destroy'
  get '/login', to: 'sessions#new'
  post '/login', to: 'sessions#create'
  delete '/logout', to: 'sessions#destroy'
end

リファクタリング前は/users/:idのようにリクエストURLにユーザーIDを記述していました。ヘルパーメソッドを作成したことにより、ユーザーIDはセッションから取得できるようになったため、リクエストURLにユーザーIDを含んでいたルーティングを変更しています。

コントローラーのリファクタリング

ユーザーのコントローラーを以下のように変更します。変更箇所が複数あるのでご注意ください。

users_controller.rb

class UsersController < ApplicationController
  # 以下を追記:コントローラーのリファクタリング①
  before_action :require_login, only: [:show, :destroy]

  ...

  def create
    # 以下を修正:コントローラーのリファクタリング②
    user = User.new(user_params)
    if user.save
      # 以下を追記:コントローラーのリファクタリング③
      log_in(user)
      redirect_to profile_path(user)
    else
      render 'new'
    end
  end

  ...

  def destroy
    # 以下を修正:コントローラーのリファクタリング④
    current_user.destroy
    redirect_to signup_path
  end

  ...
end

コントローラーのリファクタリング①

  before_action :require_login, only: [:show, :destroy]

before_actionは、アクションが実行される前の動作を定義することができます。ここでは、ユーザー情報画面および登録解除のアクションを実行する前にログイン状況を確認し、ログインしていなければログイン画面に遷移するようにしています。

コントローラーのリファクタリング②

    user = User.new(user_params)
    if user.save
      log_in(user)
      redirect_to profile_path(user)
    else
      render 'new'
    end

後述する「ビューのリファクタリング」セクションで、ユーザー情報画面に表示するユーザー情報はセッションから取得するよう変更しているため、コントローラーからインスタンス変数を渡す必要がなくなります。そのため、ユーザー情報のインスタンス変数はすべてローカル変数に変更しています。

コントローラーのリファクタリング③

      log_in(user)

ユーザー登録を行った際、同時にログイン処理も行うようにしています。

コントローラーのリファクタリング④

    current_user.destroy

ユーザー情報をデータベースからではなくセッションから取得するよう変更しています。

ビューのリファクタリング

ユーザー情報画面のビューを変更します。

show.html.erb

<h1>ユーザー情報</h1>

<%# 以下を修正:ビューのリファクタリング① %>
<p>メールアドレス:<%= current_user.email %></p>
<p><%= link_to 'ログアウト', logout_path, method: :delete %></p>
<%# 以下を修正:ビューのリファクタリング② %>
<p><%= link_to '登録解除', unsubscribe_path, method: :delete, data: { confirm: '本当に登録解除しますか?' } %></p>

ビューのリファクタリング①

<p>メールアドレス:<%= current_user.email %></p>

ユーザー情報をコントローラーから受け取ったインスタンス変数からではなくセッションから取得するよう変更しています。

ビューのリファクタリング②

<p><%= link_to '登録解除', unsubscribe_path, method: :delete, data: { confirm: '本当に登録解除しますか?' } %></p>

リクエストURLにユーザーIDを含まないようにしたため、パスの引数を削除しています。

まとめ

なるべくシンプルかつ簡潔な実装にしたつもりですが、それでも結構なボリュームになってしまったかもしれません。しかし、一度でも認証機能を自作しておくと、後々「devise」などのGemを使って認証機能を実装するときにきっと役に立つはずです。逆に言うと、一度も認証機能を自作しないまま「devise」などのGemを使って認証機能を実装しようとしても、何をしているのかさっぱりわからないということになりかねません。

認証機能を実装するには、Railsが持っている様々な機能のうちのほとんどを活用して実装する必要があります。認証機能を実装することにより、Railsに対する理解がより一層深まるのではないかと思います。

本記事を参考にして、IDとパスワードによる認証機能を実装していただければと思います。

関連記事

【Ruby】Bundlerを使ってRubyGemsを作成/公開する方法
# はじめに Bundlerを使ってRubyGemsを作成および公開する方法について説明します。Bundlerを使わずにRubyGemsを作成/公開する方法については以下の記事を参照してください。 <iframe class="hatena [...]
2022年7月12日 23:18
【Ruby】RubyGemsを作成/公開する方法
# はじめに RubyGemsを作成および公開する方法について説明します。Bundlerを使ってRubyGemsを作成する方法については以下の記事を参照してください。 <iframe class="hatenablogcard" style [...]
2022年7月11日 21:52
【Rails】M1チップ搭載MacでRuby on Railsの開発環境構築
# はじめに M1チップ搭載MacにRuby on Railsの開発環境を構築する手順を記載します。 - MacBook Air (M1, 2020) - macOS Monterey 12.3.1 # Homebrew ## [...]
2022年5月5日 11:56
【Rails】Rakeタスクの基本情報と作成・実行方法
# はじめに Railsには標準でRakeというGemが同梱されています。RakeはRubyで実装されたMake(UNIX系のOSで使用できるコマンド)のようなビルド作業を自動化するツールです。Ruby Make、略してRakeというわけですね。 [...]
2022年3月7日 22:12
【Rails】モデルに外部キーを設定する方法とよく起こるエラー内容について
# はじめに Railsでモデルに外部キーを設定する方法について説明します。 # モデルに外部キーを設定する ## リレーションシップ 今回は1つのブログ記事は複数のコメントを持つ1対多のリレーションシップを例に説明します。現在は` [...]
2022年2月10日 14:18
【Rails】Capybaraのfill_inメソッドを実行すると「既存レコードの内容+指定した内容」がセットされる事象の原因と対処【RSpec】
# はじめに RSpec + Capybaraを使用して、Railsアプリの統合テストを実装しています。とあるモデルの編集画面において、入力フォームの内容を書き換えた上で送信し、レコードが更新されることを確認します。 入力フォームの内容を書 [...]
2022年1月27日 21:22
【Rails】GitHubのセキュリティアラートで発見された脆弱性を解消する方法
# はじめに GitHubにはセキュリティアラートという機能があります。セキュリティアラートはリポジトリに含まれるライブラリやパッケージの脆弱性を定期的にチェックし、脆弱性のあるライブラリやパッケージが発見されたらアラートで知らせてくれるという機 [...]
2022年1月16日 10:36
【Rails】devise-two-factorを使った2段階認証の実装方法【初学者】
# はじめに Railsアプリで2段階認証を実装するには、「rotp」というGemを使う方法の他に、「devise-two-factor」というGemを使う方法があります。「devise-two-factor」はその名の通り、IDとパスワードによ [...]
2021年12月12日 17:58