gem sorceryを使用し、ユーザー登録、ログイン機能を実装

概要

sorceryとは、railsで作成されたアプリケーションに対し、ログイン機能を実装させるgem。
認証用gemであるdeviseと比較すると、sorceryはある程度のコードは自分で書かなければいけなく、deviseよりコードを書く量が多いがその分カスタマイズ性があり、エラーが発生したときなど自分で書いたコードなのでエラーの特定がしやすい。

導入

sorcery wikiを参考にし、gemの導入からユーザー登録、ログイン、ログアウト機能を実装する。
実行環境

  • Rails 5.2.3
  • Ruby 2.6.4
  • bootstrap 4.3.1
  • node 12.14.0

まずは、Gemfileにgem 'sorcery'を追加し、bundle installする。

gem 'sorcery'

sorceryによって追加されたジェネレータを実行しrails db:migrateすると、 userモデルとマイグレーションファイルが作成され、rails db:migrateによりusersテーブルが作成される。

$ rails g sorcery:install
      create  config/initializers/sorcery.rb
    generate  model User --skip-migration
      invoke  active_record
      create    app/models/user.rb
      insert  app/models/user.rb
      insert  app/models/user.rb
      create  db/migrate/20210429103839_sorcery_core.rb

$rails db:migrate
== 20210429103839 SorceryCore: migrating ======================================
-- create_table(:users)
   -> 0.0016s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0006s
== 20210429103839 SorceryCore: migrated (0.0024s) =============================

今回は、ユーザ登録に姓名、email、password、password confirmationを入力できるようにしたいので、データベースにlast_name、first_nameカラムをstring型で追加する。

$ rails g migration AddLastNameToUsers last_name:string
#マイグレーションファイルを編集
  def change
    add_column :users, :last_name, :string
  end
rails db:migrate  #first_nameも同様に作成する。

SQLで作成されているか確認。

sqlite> PRAGMA TABLE_INFO(users);
0|id|integer|1||1
1|email|varchar|1||0
2|crypted_password|varchar|0|NULL|0
3|salt|varchar|0|NULL|0
4|created_at|datetime|1||0
5|updated_at|datetime|1||0
6|last_name|varchar|0||0
7|first_name|varchar|0||0

ユーザーの作成

ジェネレータを使用し、usersコントローラとビューの作成。

$rails g scaffold_controller user email:string crypted_password:string salt:string
     create  app/controllers/users_controller.rb
      invoke  erb
      create    app/views/users
      create    app/views/users/index.html.erb
      create    app/views/users/edit.html.erb
      create    app/views/users/show.html.erb
      create    app/views/users/new.html.erb
      create    app/views/users/_form.html.erb

viewの作成

form_withについて

form_withとは、モデルオブジェクトを使ってhtmlのフォームを作成するためのヘルパーメソッドである。 form_withにはmodel:とurl:オプションがあり使い分ける必要がある。 入力された情報がデータベースへ保存しない(モデルとフォームが紐付かない)場合はurl:オプションを使う。

<%= form_with url: "パス" do |form| %>
<% end %>

データベースへ保存するときは、model:オプションを使う。

<%= form_with model: モデルクラスのインスタンス do |form| %>
<% end %>

今回は、新規登録内容をデータベースへ保存する必要があるのでmodel:オプションを使用する。form_withにmodel: @userという引数を渡すことで、モデルとフォームを対応づける。また、form_withはデフォルトでajax通信になるのでlocal: trueをつける。

<div class="container">
  <div class="row">
    <div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
      <h1>ユーザー登録</h1>
      <%= form_with model:@user, local: true do |f| %>
        <div class="form-group">
          <%= f.label :last_name %>
          <%= f.text_field :last_name, class: 'form-control', id: 'user_last_name' %>
        </div>
        <div class="form-group">
          <%= f.label :first_name %>
          <%= f.text_field :first_name, class: 'form-control', id: 'user_first_name' %>
        </div>
        <div class="form-group">
          <%= f.label :email %>
          <%= f.text_field :email, class: 'form-control', id: 'user_email' %>
        </div>
        <div class="form-group">
          <%= f.label :password %>
          <%= f.password_field :password, class: 'form-control', id: 'user_password' %>
        </div>
        <div class="form-group">
          <%= f.label :password_confirmation %>
          <%= f.password_field :password_confirmation, class: 'form-control', id: 'user_password_confirmation' %>
        </div>
        <%= f.submit '登録する', class: 'btn btn-primary'%>
        <%end%>
      <div class='text-center'>
      <%= link_to 'ログインページへ', login_path%>     
      </div>
    </div>
  </div>
</div>

ログイン状態に応じてヘッダー表示を切り替える。

logged_in?メソッドを使用する。 ログイン前はログインボタン、ログイン後は一覧やログアウトボタンを表示するようになる。

#...
   <body>
   <% if logged_in? %>
     <%= render 'shared/header'%>
   <% else %>
     <%= render 'shared/before_login_header'%>
   <% end %>
   <%= yield %>
   <%= render 'shared/footer'%>
  </body>
</html>

_form.html.erbにパスワードを保持する仮想フィールドを追加

<div class="field">
   <%= form.label :password %><br />
   <%= form.password_field :password %>
</div>
<div class="field">
   <%= form.label :password_confirmation %><br />
   <%= form.password_field :password_confirmation %>
</div>

controllerの作成

ユーザー登録後はログイン画面へリダイレクトするようにし、ストロングパラメータでは、last_name,first_name,password,password_confirmationを受け取れるようにする。

ストロングパラメータについて

Rails4から導入された「Mass-assignment」という脆弱性へのセキュリティ対策である。テーブルに登録されるすべてのデータはストロングパラメータによる検証をクリアしなければ登録できなくなっている。例えば、@user =User.new(user_params)を(params[:user])に変更して実行すると「ActiveModel::ForbiddenAttributesError 」という例外が発生してしまう。user_paramsメソッドに記載されたコードは「paramsが:userというキーを持っていて、かつparams[:user]が、:email, :last_name, :first_name, :password, :password_confirmation, :crypted_password, :saltというキーを持つハッシュである」ことを確認し、登録できるカラムを特定し、それ以外のカラムへのアクセスを禁止している。よって、許可されたカラムしか登録、更新ができなくなる。

class UsersController < ApplicationController
  before_action :set_user, only: %i[show edit update destroy]
  skip_before_action :require_login, only: %i[new create index]
#...
  def new
    @user = User.new 
#インスタンス変数にuser#newアクションからviewに渡したいデータを入れる
  end

  def create
    @user = User.new(user_params) 
#user#createアクションで@userにuser_paramsに入ったデータを代入。
    if @user.save #データを保存
      redirect_to login_path, notice: 'success'
#login_path ヘルパーメソッドを使用。保存が成功したらログインページへ
    else
      render :new #保存が失敗したら登録画面へ
    end
  end
#...

  private

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

  def user_params
    params.require(:user).permit(:email, :last_name, :first_name, :password, :password_confirmation, :crypted_password, :salt)
  end
end

Modelの作成

モデルファイルにバリデーションを設定する。authenticates_with_sorcery!はuserモデルにsorceryによる認証機能をもたせている。 passwordとpassword_confirmationはuserモデルのカラムには存在しない。crypted_passwordカラムの仮想属性である。sorceryによってcrypted_passwordカラムの内容であると認識される。

class User < ApplicationRecord
  authenticates_with_sorcery!
  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
#ここでcrypted_passwordとして認識される。
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }
  validates :email, presence: true, uniqueness: true
  validates :last_name, presence: true, length: { maximum: 250 }
  validates :first_name, presence: true, length: { maximum: 250 }
end

ログイン、ログアウト機能の実装

ジェネレータでuser_sessions_controllerを作成する。

$ rails g controller UserSessions new create destroy 
      create  app/controllers/user_sessions_controller.rb
      invoke  erb
      create    app/views/user_sessions
      create    app/views/user_sessions/new.html.erb
      create    app/views/user_sessions/create.html.erb
      create    app/views/user_sessions/destroy.html.erb
Routerを定義する。

ルーティングを以下のように定義。

Rails.application.routes.draw do
  root to: 'users#index'
  get '/login', to: 'user_sessions#new', as: :login
  post '/login', to: 'user_sessions#create'
  post '/logout', to: 'user_sessions#destroy', as: :logout
  resources :users
end

controllerの作成

redirect_back_or_toメソッドはsorceryでフレンドリーフォーワーディングを実現するための機能である。 ログインが必要なページに未ログイン状態でアクセスした場合、ログイン画面に遷移させてログイン後はログインが必要なページへ飛ばす。

class UserSessionsController < ApplicationController
  skip_before_action :require_login, only: %i[new create]
  def new; end

  def create
    @user = login(params[:email], params[:password])
#emailによるユーザ検索、passwordの検証を行いUserレコードのid値をセッションに格納。
    if @user
      redirect_back_or_to root_path, notice: 'Login successful!!!'
#ログイン後はトップページへリダイレクト
    else
      flash.now[:alert] = 'Login failed'
      render action: 'new'
    end
  end

  def destroy
    logout
    redirect_to(root_path, notice: 'Logged out!')
#ログアウト後トップページへリダイレクト
  end
end

セッションviewの作成

ログインフォームはモデルと紐付かないので、form_withのオプションはurl:を使用する。フォーム送信先は/loginなのでlogin_pathとする。

<%= link_to 'Back', users_path %>
<div class="container">
 <div class="row">
   <div class=" col-md-10 offset-md-1 col-lg-8 offset-lg-2">
     <h1>ログイン</h1>
     <%= form_with url: login_path, local: true do |f| %>
       <div class="form-group">
         <%= f.label :email %>
         <%= f.text_field :email, class: "form-control" %>
       </div>
       <div class="form-group">
         <%= f.label :password %>
         <%= f.password_field :password, class:"form-control" %>
       </div>
       <div class="actions">
         <%= f.submit "ログイン", class: "btn btn-primary" %>
       </div>
     <% end %>
     <div class='text-center'>
       <%= link_to '登録ページへ', new_user_path %>
     </div>
   </div>
 </div>
</div>

これでユーザー登録、ログイン・ログアウト機能の完成。