見出し画像

マネージドクラウドでRails 6のアプリケーションを運用しよう 〜第2回 認証機能を作成する

ごきげんよう、開発チームのtascriptです!
こちらの記事は、「マネージドクラウドでRails 6のアプリケーションを運用しよう」の連載シリーズ第2回です。

前回ご紹介しました記事はご覧いただけましたでしょうか?
まだご覧になられていない方は下記のリンクから過去の記事をご覧いただけますと幸いです!

【記事一覧】
マネージドクラウドでRails 6のアプリケーションを運用しよう 〜第1回 Rails 6のアプリケーションをデプロイする〜

前回の振り返り

前回は、Rails6のアプリケーションをマネージドクラウドにデプロイするところまでを紹介しました。無事にデプロイは出来ましたでしょうか?今回からはRails6 のアプリケーションを開発しつつ、マネージドクラウドにデプロイしていくまでを紹介していきます!

作るもの

ブログを投稿するアプリケーションを作ります(わーい)!!

・認証機能
・ブログの投稿・管理
・その他

といった機能の開発について今後ご紹介していきます。

今回の目的

今回は認証機能を実装してみましょう!多くのWebアプリケーションで必要となるログイン、ログアウトの機能を実装してWebアプリケーションに安全な認証機能を導入することを目的とします。

開発を始める前に

連載記事を進めていくことで作成できるアプリケーションのソースコードを下記のリポジトリにて公開しています!この連載では、簡単に開発方法を紹介していますが、より詳細な情報が知りたい場合など、補足資料としてご利用いただけますと幸いです。

今回の開発手順は以下のとおりです。

1. ユーザー情報を保存するテーブルの用意
2. アカウント作成機能を実装
3. ログイン・ログアウト機能を実装

準備はよろしいでしょうか?それでは、早速開発を始めていきましょう!

ユーザー情報を保存するテーブルの用意

ユーザー認証のためには、ユーザー情報を保存するためのテーブルを用意する必要があります。今回は簡単にメールアドレスとパスワードによる認証機能を実装します。Usersテーブルを用意し、以下のようなマイグレーションファイルを用意します。

class CreateUsers < ActiveRecord::Migration[6.0]
 def up
   create_table :users, id: false do |t|
     t.string :id, limit: 36, null: false, primary_key: true
     t.string :name, null: false
     t.string :email, null: false
     t.string :password_digest, null: false
     t.string :token
     t.datetime :created_at, null: false, default: -> { 'CURRENT_TIMESTAMP' }
     t.datetime :updated_at, null: false, default: -> { 'CURRENT_TIMESTAMP' }
   end
   add_index :users, :email, unique: true
   add_index :users, :token, unique: true
 end

 def down
   remove_index :users, :email
   drop_table :users
 end
end

重要なのはemailカラムとpassword_digestカラムです。ちなみにidカラムはauto incrementではなくuuidを使用するために宣言していたり、利用制限のためにtokenカラムを用意していますが、今回は気にしなくても大丈夫です!
マイグレーションファイルの用意ができましたら、下記のコマンドを実行し、マイグレーションを実施してください。

$ bundle exec rails db:migrate

ロールバックしたい場合は、下記のコマンドを実行してください。

$ bundle exec rails db:rollback

これでテーブル作成は完了です!

アカウント作成機能を実装

1. ルーティングの設定

用意するのは、以下のルーティングです。アカウント作成に必要なルーティングと同時に今後必要になるログイン、ログアウト用のルーティングも定義しておきます。

Rails.application.routes.draw do
 root   'home#index'
 get    'login',              to: 'sessions#new'     # ログインページ
 post   'login',              to: 'sessions#create'  # ログイン
 delete 'logout',             to: 'sessions#destroy' # ログアウト
 get    'signup',             to: 'users#new'        # アカウント作成画面
 post   'signup',             to: 'users#create'   # アカウント作成     
end

2. Userモデルの用意
Userモデルを用意し、has_secure_passwordを宣言することでUserモデルに対しpassword、password_confirmation属性を設けることができます。password属性により対象のレコードから生のパスワードが参照できるようになります。同時にpassword_digestカラムにはハッシュ化したパスワードが保存されるようになり、安全なデータ操作が可能になります。さらに、インスタンスメソッドとしてauthenticateメソッドもモデルに付与されます。このメソッドの引数としてパスワードを採用すると、対象ユーザーのパスワードと一致するかどうかをbooleanで返すことができます。

class User < ApplicationRecord
  has_secure_password
  ・
  ・
  ・
end

今回は追加機能として、サロゲートキーとなるidカラムをuuidで表現したり、カラムに暗号化した値を保存するようにしていますが、ここはお好みで実装してください。詳しい実装は以下のリンクよりご確認いただけますと幸いです!

3. フォームとコントローラーの用意
新規ユーザー作成のためのフォームとUsersコントローラーを用意します。

まずはUsersコントローラを作成します。フォーム表示に必要なnew、データ保存に必要なcreateといった2つのアクションを用意しましょう。データ保存を安全に実施するために、ストロングパラメーターを付与することも忘れずにお願いします!

class UsersController < ApplicationController
 before_action :confirm_auth, only: %i[new create]

 def new
  @user = User.new
 end
  
 def create
   @user = User.new(user_params)
   User.all.each do |user|
     next unless user.email == user_params[:email]

     redirect_to(controller: :users, action: :new) && return
   end
   if @user.save
     flash[:success] = 'ユーザーの登録が完了しました。'
     redirect_to(controller: :home, action: :index) && return
   else
     flash[:danger] = 'ユーザーの登録に失敗しました。'
     render 'new' && return
   end
 end
 
 def user_params
  params.require(:user).permit(:name, :email, :password, :password_digest)
 end
end

続いて、フォームです。form helperとしてform_withを使用しています。form_withはform_tagとform_forの機能を統合したヘルパーであり、Rails 5.1以上で使用可能です。下記のコードからも対象となるモデルとリクエスト先を同時に指定できることがわかります。form_withはデフォルトでAjax通信を実施するようになっていますので、local :true の設定をすることでAjax通信を切るようにしておきます。

<h1>アカウント作成</h1>
<div class="form-area">
 <%= form_with model: @user, url: signup_path, local: true do |f| %>
   <%= f.label :name %>
   <%= f.text_field :name, class: "form-text" %>
   <%= f.label :email %>
   <%= f.email_field :email, class: "form-text" %>
   <%= f.label :password %>
   <%= f.password_field :password, class: "form-text" %>
   <div class="form-submit">
     <%= f.submit "作成" %>
   </div>
 <% end %>
</div>

ログイン・ログアウト機能を実装

1. フォーム、コントローラー、ヘルパーの用意

ログイン用フォーム、Sessionsコントローラ、Sessionsヘルパーを用意します。

今回はログイン情報をCookieに持たせたいので、SessionsヘルパーにCookieの操作を記述します。log_inメソッドでは、Cookieにはユーザー情報の識別子を保存します。Cookieの有効期限は2時間に設定し、値を暗号化するために署名付きCookieにします。下記コードにはログイン制限のためにtokenカラムの値も保存していますが、ここはお好みで実装してください!

current_userメソッドではCookieからユーザー情報を取得し、インスタンス変数の@current_userに保存します。SessionsヘルパーをApplicationControllerにIncludeすることで、ApplicationControllerを継承する全てのコントローラでログイン中のユーザー情報を確認することができます。

logged_in?メソッドではログインの確認、log_outメソッドではCookieの削除を実施します。

module SessionsHelper
 def log_in(user)
   expires = 2.hours.from_now
   cookies.signed[:user_id] = {
     value: user.id,
     httponly: true,
     secure: Rails.env.production?,
     expires: expires
   }
   cookies.signed[:user_token] = {
     value: user.token,
     httponly: true,
     secure: Rails.env.production?,
     expires: expires
   }
 end
 def current_user
   user = User.find_by(id: cookies.signed[:user_id])
   @current_user = user if user&.authed?(cookies.signed[:user_token])
 end
 def logged_in?
   current_user
   !@current_user.nil?
 end
 def log_out
   cookies.delete(:user_id)
   cookies.delete(:user_token)
   @current_user = nil
 end
end

続いて、Sessionsコントローラを用意します。createメソッドでログインdestroyメソッドでログアウトを実装しています。前述したSessionsヘルパーに用意したlog_inメソッドとlog_outメソッドを利用しています。params[:session][:password]については後述します。

class SessionsController < ApplicationController
 before_action :confirm_auth, only: %i[new create]
 def new; end
 def create
   user = User.all.find { |u| u.email == params[:session][:email] }
   if user&.authenticate(params[:session][:password])
     log_in user
     flash[:success] = 'ログインに成功しました。'
     redirect_to(controller: :dashboard, action: :index)
   else
     flash[:danger] = 'メールアドレスもしくはパスワードに誤りがあります。'
     render 'new'
   end
 end
 def destroy
   log_out if logged_in?
   redirect_to login_path
 end
end

ログイン用フォームです。新規ユーザー作成と同様に、ここでもユーザーform_withを使用していますが、Sessionsコントローラには対応するモデルがないので、scopeを利用してフォームを作成しています。scopeに「session」を付与することで、フォームタグのaction属性に「/sessions」、inputタグのname属性に「sesssion」プレフィックスを付与します。前述したparams[:session][:password]のようにフォームのパラメータを取得していたのはこのためです。

<h1>ログイン</h1>
<div class="form-area">
 <%= form_with scope: :session, url: login_path, local: true do |f| %>
   <%= f.label :email %>
   <%= f.email_field :email, class: "form-text" %>
   <%= f.label :password %>
   <%= f.password_field :password , class: "form-text"%>
   <%= link_to "アカウントを作成する", signup_path, method: "get", class: "signup-link" %>
   <div class="form-submit">
     <%= f.submit "ログイン" %>
   </div>
 <% end %>
</div>

2. ApplicationControllerを改修

SessionsヘルパーのIncludeと、ログイン状況の確認用メソッドを用意します。validate_authメソッドではログイン状況を確認し、ログインしていなければ、Cookieを削除しログイン画面にリダイレクト、confirm_authメソッドではログインしている場合にダッシュボードページにリダイレクトします。ダッシュボードページについては次回に詳細を記載しますが、ログイン後にアクセスできるページです。

class ApplicationController < ActionController::Base
 include SessionsHelper
 private
 def validate_auth
   unless logged_in?
     log_out
     redirect_to(controller: :sessions, action: :new)
   end
 end
 def confirm_auth
   redirect_to(controller: :dashboard, action: :index) if logged_in?
 end
end

重要なのは、validate_authメソッドで、コントローラのうち、ログイン後にのみ使用するアクションを実施する前にログイン状況を確認するために使用します。例えば、以下のようにコントローラに対しbefore_actionを定義することで、アクション実施前にログイン状況を確認し、ログインしていなければ、Cookieを削除しログイン画面にリダイレクトします。

以上で認証機能が完成しました!お疲れ様です!

振り返り

今回はRails 6で作成する認証機能についてご紹介しました。結構情報量が多くて混乱した方もいらっしゃるかもしれませんが、ここまでくれば、あとはメイン機能の開発に集中できるので諦めずにチャレンジしてみてください!

次回は、ブログの投稿機能の実装について紹介しますので、どうぞお楽しみに!それでは、また次回お会いしましょう!