【Rails7】ユーザー認証機能(ログイン・ログアウト)を実装するまでの一連の流れ

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 使用)

ソースコード

GitHub
GitHub - hirokirokki0820/user-authentication Contribute to hirokirokki0820/user-authentication development by creating an account on GitHub.
GitHub
GitHub - hirokirokki0820/alpha-blog Contribute to hirokirokki0820/alpha-blog development by creating an account on GitHub.

ユーザー認証を実装する全体的な流れ

大まかな流れを以下に示します。

  1. ユーザーモデルを作成する(name, email, password)
  2. パスワードのハッシュ化(BCrypt)とバリデーションの追加
  3. ユーザーの新規登録フォーム作成
  4. sessionsコントローラーの作成
  5. ログイン・ログアウトを識別するメソッドを定義
  6. ユーザーログイン用のフォーム作成
  7. 許可のないユーザーによるアクセスを制限

こんな感じです。

簡単なログイン機能を実装するだけでもかなりの分量ありますが、一つ一つ地道に進めていきましょう。

ユーザーモデルを作成する

今回作成するユーザーモデルのカラムは以下の通りです。

カラム名
namestring
emailstring
password_digeststring

パスワードは、後ほどハッシュ化のため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】各種idをランダムな文字列に変換してPrimary keyとして使う Railsでデータベースに値を登録すると、デフォルトでは各テーブルのid(integer型)が1から順番に割り当てられるようになっています。 しかし、このままだと(以下の例...

作成するテーブルのカラム名に間違いがないことを確認したら、以下のコマンドでデータベースを作成します。

$ 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.rbhas_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 については以下の記事を参考にして見てください。

あわせて読みたい
【Rails7】Semantic UI をセットアップする方法 つい先日、Rails を使って自作Webアプリを作る上で何か良いデザイン用のCSSフレームワークがないか探していたところ、「Semantic UI」という神がかったフレームワークを...

あと、ここで詳しいコードを書くと長くなってしまうので、ソースコードはGitHubに置いておきます。

GitHub
user-authentication/app/views/users at main · hirokirokki0820/user-authentication Contribute to hirokirokki0820/user-authentication development by creating an account on 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.rbrequire_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.rbrequire_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」機能の実装を行なっていきたいと思います。

それができたら、メール認証によるアカウント有効化も実装してみたいですね。

実装できたらまたブログに書き残そうと思います。

【続き】Cookieを使ってログイン状態を保持する(Remember me 機能の実装)

あわせて読みたい
【Rails7】Cookieを使ってログイン状態を保持する(Remember me 機能の実装) Railsチュートリアル第8章にて、基本的なログイン・ログアウト機能の作成はできましたが、現状だとブラウザを閉じるとセッション情報が失われてしまいます。 (ブラウザ...
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

愛知の34歳。無職で暇になり始めたプログラミング(Ruby on Rails)の忘備録をまとめたブログです。最近は別にやりたいことができたのでプログラミングほぼやっていません。気が向いたらまた再開するかも。僕の日常はメインブログの方で更新しています。

コメント

コメントする

目次