Railsでサービスを開発しようとなると避けて通れない(と思う)のが、ユーザーの認証機能。
認証機能を1から作るのはかなり大変ですが、幸いRailsにはDeviceという簡単に認証機能を実装できるgem(ライブラリ)があります。
とはいえ、Deviseはある意味、認証機能の一つの完成形とも言える。
なので、カスタマイズしようと思っても一素人が簡単に手出しできる代物ではないし、そうなると「もう中身のことは知らなくていいや」と何も知らないままDeviseに頼り切ってしまう。
それは一プログラマーとしては実によろしくない。
「基本的なユーザー認証の仕組みだけでもしっかり理解しておきたい…」
ということで今回はユーザーのログイン、ログアウト機構を一通り作ってみたので、その手順をメモとして残しておこうと思います。
(今回作ったユーザー認証機能はRailsチュートリアルの第8章を参考にしました)
開発環境
- Ruby 3.1.2
- Ruby on Rails 7.0.3
- M1 Macbook Air 2020
- mac OS Monterey (ver. 12.4)
- ターミナル bash (Rosetta 2 使用)
ソースコード
ユーザー認証を実装する全体的な流れ
大まかな流れを以下に示します。
- ユーザーモデルを作成する(name, email, password)
- パスワードのハッシュ化(BCrypt)とバリデーションの追加
- ユーザーの新規登録フォーム作成
- sessionsコントローラーの作成
- ログイン・ログアウトを識別するメソッドを定義
- ユーザーログイン用のフォーム作成
- 許可のないユーザーによるアクセスを制限
こんな感じです。
簡単なログイン機能を実装するだけでもかなりの分量ありますが、一つ一つ地道に進めていきましょう。
ユーザーモデルを作成する
今回作成するユーザーモデルのカラムは以下の通りです。
カラム名 | 型 |
name | string |
string | |
password_digest | string |
パスワードは、後ほどハッシュ化のためBCryptを使う関係でpassword_digest
というカラム名にします。
$ rails g model User name:string email:string password_digest:string
作成されたマイグレーションファイルの中身を確認します。
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password_digest
t.timestamps
end
end
end
もし、ユーザーidにランダムな文字列を与えたい場合は、id属性をstringにするよう指定します。
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users, id: :string do |t|
t.string :name
t.string :email
t.string :password_digest
t.timestamps
end
end
end
詳しくは下記記事にて。
作成するテーブルのカラム名に間違いがないことを確認したら、以下のコマンドでデータベースを作成します。
$ rails db:migrate
もし、マイグレーションの適用を取り消したい場合は以下のコマンドで取り消す(マイグレーション実行前まで戻る)ことができます。
$ rails db:rollback
ユーザー用のルートを設定
ユーザーモデルを作成したら、まずはルートの設定を行います。
ルートの設定はresources :users
とすることで、基本的なCRUD操作に必要なルートを自動的に生成してくれます。
ただ、ユーザー新規作成時のパスの見栄えがよくない(http://localhost:3000/users/new)ので、新規作成時のルートのみ別口で以下のように指定します。
Rails.application.routes.draw do
root "home#index"
get "about", to: "home#about"
#ユーザー用のルート設定("users#new"のパスのみ rootのURL/signup としたい)
get "signup", to: "users#new"
resources :users, except: [:new]
end
こうすると、ユーザー新規作成時のパスはhttp://localhost:3000/signup
となり、先ほどに比べると見栄えが良くなっているかと思います。
パスワードのハッシュ化(BCrypt)
パスワードのハッシュ化にはBCryptというgemを用います。
まずは、Gemfileに以下のように記述し(コメントアウトされている場合は外し)、$ bundle install
します。
gem "bcrypt" # 追記、またはコメントアウトを外す
続いて、user.rb
にhas_secure_password
を定義します。
class User < ApplicationRecord
has_secure_password
end
こうすることで、次のような機能が使えるようになります。
- ハッシュ化したパスワードを、データベース内のpassword_digest属性に保存できるようになる
- password、およびpassword_confirmation属性が使えるようになる
- 上記2属性のpresense、および値が一致するかどうかのバリデーションも追加される
- authenticateメソッドが使えるようになる
ただし、Userモデル内にpassword_digest
という属性が含まれていることが条件となります。
バリデーションの追加
不正なデータがデータベースに保存されないよう、データをチェックするためにUserモデルにバリデーションを追加します。
class User < ApplicationRecord
# email オブジェクトが保存される時点で小文字に変換する
before_save { self.email = email.downcase }
# name のバリデーション
validates :name, presence: true, length: { maximum: 25 }
# email のバリデーション
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true,
uniqueness: { case_sensitive: false },
length: { maximum: 105 },
format: { with: VALID_EMAIL_REGEX }
# password のバリデーション
has_secure_password
validates :password, presence: true,
length: { minimum: 6 },
allow_nil: true
end
上記のバリデーションでは、それぞれ以下のチェックを行っています。
- nameは存在性と最大入力文字数の制限
- emailは存在性と最大入力文字数の制限、大文字小文字を区別しない、メールアドレスが有効な値かどうかチェック
- passwordは最低入力文字数の設定
ここで、email のバリデーション中にあった以下のコードを正規表現と言います。
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
ここで詳しく説明はしませんが、要は入力された文字がちゃんとemailアドレスのフォーマットになっているかどうかをチェックするためのコードです。
ちなみに、正規表現が正しく機能しているかどうかは、RubularというWebサイトで試すことができます。
ユーザー新規登録用のフォームを作成する
ログイン、ログアウトを実装するために、まずはユーザーの登録が先決です。
ここでは、ユーザー新規登録用のフォームを作成していきます。
(本記事のメインはログイン、ログアウト機能についてなので、このパートはサクッといきます)
usersコントローラーを作成する
以下のコマンドでusersコントローラーを作成します。
$ rails g controller users
コントローラーには以下のように記述します。
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy ]
def index
@users = User.all
end
def show
end
def new
@user = User.new
end
def edit
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
redirect_to user_url(@user), notice: "ようこそ、Sample Blog へ!"
else
render :new, status: :unprocessable_entity # rails7 から必須のオプション
end
end
end
def update
if @user.update(user_params)
redirect_to user_url(@user), notice: "ユーザーアカウントを編集しました。"
else
render :edit, status: :unprocessable_entity # rails7 から必須のオプション
end
end
end
def destroy
@user.destroy
redirect_to users_url, notice: "ユーザーアカウントを削除しました。", status: :see_other # rails7 から必須のオプション
end
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end
新規登録用のフォームを作成する
とりあえず、どんな感じでもいいので新規登録用のフォームをちゃちゃっと作っちゃいましょう。
僕はSemantic UIというものを試してみたく、以下のようにして見ました。
Semantic UI については以下の記事を参考にして見てください。
あと、ここで詳しいコードを書くと長くなってしまうので、ソースコードはGitHubに置いておきます。
ユーザー認証(ログイン・ログアウト)を実装する
さて、いよいよユーザー認証を実装するお時間です。
ユーザー認証を実装する流れとしては、
- session用のルーティング設定を行なう
- sessionsコントローラーを作成する
- ユーザーのログイン・ログアウトを判断するメソッドを定義する
- ユーザーログイン用のフォームを作成する
- 許可のないユーザーに対する機能的な制限(編集、削除の制限など)
という感じです。
session用のルーティング設定を行なう
まずはsession用のルーティング設定を行います。
基本的なログイン機能を実装する上で必要なアクションはnew
create
destroy
の3つだけなので、以下のように付け加えていきます。
Rails.application.routes.draw do
root "home#index"
get "about", to: "home#about"
get "signup", to: "users#new"
resources :users, except: [:new]
# Session用のルートティングを設定
get "login", to: "sessions#new"
post "login", to: "sessions#create"
delete "logout", to: "sessions#destroy"
end
sessionsコントローラーを作成する
以下のコマンドでsessionsコントローラーを作成します。
$ rails g controller sessions new
続いて、sessionsコントローラーに以下のコードを記述していきます。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
# bcrypt の authenticateメソッドでパスワードの照合を行なう
if user && user.authenticate(params[:session][:password])
log_in(user)
redirect_to user
else
flash.now[:error] = "ログインに失敗しました"
render "new", status: :unprocessable_entity
end
end
def destroy
log_out
redirect_to root_path, status: :see_other
end
end
上記のcreateの部分でログインフォームで入力した値がデータベースの情報と一致するかどうかをチェックしています。
ユーザー認証が通ったら、先ほど定義したlog_in(user)メソッドで一時セッションを作成し、その中に値(ユーザーID)を保存します。
この、一時セッションの中にユーザーIDを保存することで、そのユーザーはログインしている状態になります。(使用しているブラウザ上で)
逆に、セッションに保存されている情報を削除してやればログアウトとなります。
ユーザーのログイン・ログアウトを判断するメソッドを定義する
続いて、ユーザーが現在ログイン・ログアウトしているかどうかを判断するためのメソッドを定義します。
メソッドはapp/helpers/sessions_helper.rb内に、以下のように記述します。
module SessionsHelper
# ログイン時にセッションIDを付与する
def log_in(user)
session[:user_id] = user.id
end
# 現在ログインしているユーザーのユーザー情報を取得する
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
# ユーザーがログインしているかどうかをチェックする
def logged_in?
!current_user.nil?
end
# ログアウトする(セッション情報を削除する)
def log_out
session.delete(:user_id)
@current_user = nil
end
end
また、これらのメソッドを各種コントローラー内でも使えるようにしたいので、先ほど定義したメソッドをapplication_controller.rb
内に読み込むための記述をします。
class ApplicationController < ActionController::Base
include SessionsHelper
end
これで、セッション用のビュー内、および全てのコントローラー内で先ほど定義したメソッドを使用することができます。
ユーザーログイン用のフォームを作成する
一通りのログイン・ログアウトの動線を張り巡らせたところで、ログイン用のフォームを作っていきましょう。
ログイン用のフォーム(view)は先に形だけ作っておいてもいいですね。
ログインからログアウトまでの流れはこんな感じです。
ログイン時・ログアウト時で表示するviewの内容を変える
上の動画でお気づきかもしれませんが、ログイン・ログアウト時でナビゲーションバーに表示されている内容が若干異なります。
例えば、ログイン中は「ログアウト」ボタンを表示して、ログアウト時には逆に「ログイン」「サインアップ」ボタンを表示する、
といった感じに表示するviewの内容を切り替える必要があります。
「ログイン」「ログアウト」ボタンがナビゲーションバーに配置されている場合は、ナビゲーションバー用のviewファイルに以下のように記述します。
<% if logged_in? %>
<%= link_to "Log out", logout_path, class: "ui inverted button", data: { turbo_method: :delete, turbo_confirm: " 本当にログアウトしますか?"}
<% else %>
<%= link_to "Log in", login_path, class: "ui inverted button" %>
<%= link_to "Sign up", signup_path, class: "ui inverted button" %>
<% end %>
ログイン状態かどうかをチェックするlogged_in?
メソッドで条件分岐し、ログイン時・ログアウト時の表示分けをしておきましょう。
許可のないユーザーによる編集、削除の制限(require_user, require_same_userメソッドの定義)
ここまで、一通りのログイン・ログアウトの機能を実装し終えました。
ただし、まだ完成ではありません。
見かけ上はログイン・ログアウトで表示分けされているように見えますが、
今のままではログインユーザーにしか許可されていないURLに、ログインしていないユーザーがアクセスできてしまうからです。
また、ユーザーのプロフィール情報を他人が編集できてしまうという問題もあります。
例えば、以下のようにログインユーザーが他人のプロフィールページのURLにedit
を付け加えるだけで、その人のユーザー情報を編集できてしまうのです。
そんなことがまかり通ったら大変なことが起こりますw
そこで、これらの問題を解決するために、2つのメソッドを定義します。
1つ目は、sessions_helper.rb
にrequire_user
メソッドを付け加えます。
module SessionsHelper
# ログイン時にセッションIDを付与する
def log_in(user)
session[:user_id] = user.id
end
# 現在ログインしているユーザーのユーザー情報を取得する
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
# ユーザーがログインしているかどうかをチェックする
def logged_in?
!current_user.nil?
end
# --------- 追記 ----------
# ログインしていないユーザーがアクセスしてきた場合、ログイン画面にリダイレクトさせる
def require_user
if !logged_in?
flash[:alert] = "ログインしてください。"
redirect_to login_path
end
end
# ------------------------
# ログアウトする(セッション情報を削除する)
def log_out
session.delete(:user_id)
@current_user = nil
end
end
そして、2つ目はusers_controller.rb
にrequire_same_user
をprivateメソッドとして定義します。
def require_same_user
if current_user != @user
flash[:alert] = "許可されていない操作です。プロフィールの編集、削除は作成者ご自身のみ可能です。"
redirect_to @user
end
end
これは、ログインユーザーが他のユーザーのプロフィールを編集(~/users/user_id/edit
へアクセス)しようとした場合は、その操作を禁止するというメソッドです。
これら2つのメソッドを、before_action
を用いて制限したいアクションに対して適用させます。
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy ]
#---------- 追記 -----------
before_action :require_user, except: [:new, :create]
before_action :require_same_user, only: [:edit, :update, :destroy]
#--------------------------
def index
@users = User.all
end
def show
end
def new
@user = User.new
end
def edit
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
redirect_to user_url(@user), notice: "ようこそ、Sample Blog へ!"
else
render :new, status: :unprocessable_entity # rails7 から必須のオプション
end
end
end
def update
if @user.update(user_params)
redirect_to user_url(@user), notice: "ユーザーアカウントを編集しました。"
else
render :edit, status: :unprocessable_entity # rails7 から必須のオプション
end
end
end
def destroy
@user.destroy
redirect_to users_url, notice: "ユーザーアカウントを削除しました。", status: :see_other # rails7 から必須のオプション
end
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
#---------- 追記 -----------
def require_same_user
if current_user != @user
flash[:alert] = "許可されていない操作です。プロフィールの編集、削除は作成者ご自身のみ可能です。"
redirect_to @user
end
end
#--------------------------
end
また、表示側の制限もしておきましょう。
ユーザープロフィール画面に「編集」「削除」リンクを設けている場合は、他のユーザーのプロフィール画面に「編集」「削除」リンクが表示されないように以下のように条件分岐をかけておきます。
<% if logged_in? && @user == current_user %>
<%= link_to "編集する", edit_user_path(@user), class: "ui inverted green button" %>
<%= link_to "削除する", @user, class: "ui inverted red button", data: { turbo_method: :delete, turbo_confirm: "本当に削除しますか?" } %>
<% end %>
これで一通りのログイン・ログアウト機能の実装ができました。
ログインしていないユーザーが利用者一覧情報(およびプロフィール詳細)にアクセスしようとすると、「ログインしてください」というメッセージと共にログイン画面にリダイレクトされているのがわかります。
また、こちらではログインしているユーザーが他人のプロフィール情報を編集しようとしてもエラーメッセージと共に弾かれることが確認できます。
以上で、基本的なユーザー認証は完成です。
ユーザー認証(ログイン・ログアウト)のテストを行なう
あとは、ユーザー認証が問題なく行えているかどうかをテストします。
テストの作成方法については下記記事をご参考に。
以上です。
今回は簡単なユーザー認証を実装する流れを書きましたが、まだまだ実際のサービスで使える代物ではありません。
というのも、今回はログインの識別をRails側で作成した一時セッションの値の有無で行っていますが、ブラウザのウィンドウを閉じた瞬間(ブラウザをkillした瞬間)に一時セッションの情報は失われていまいます。
実際のWebサイトではCookiesに値を保存して、一度ログインしたらブラウザを閉じても半永久的に(もしくは一定期間)ログイン状態を維持できるようになっていますよね。
今後は、Cookieを用いたユーザー認証および「remember me」機能の実装を行なっていきたいと思います。
それができたら、メール認証によるアカウント有効化も実装してみたいですね。
実装できたらまたブログに書き残そうと思います。
コメント