はじめに
大抵の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
というヘルパーメソッドは、記述するだけでセキュアなパスワードを扱えるようになる特殊なメソッドです。このヘルパーメソッドを使うと、password
とpassword_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_login
、log_in
、log_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とパスワードによる認証機能を実装していただければと思います。