gem sorceryを使用し、ユーザー登録、ログイン機能を実装
概要
sorceryとは、railsで作成されたアプリケーションに対し、ログイン機能を実装させるgem。
認証用gemであるdeviseと比較すると、sorceryはある程度のコードは自分で書かなければいけなく、deviseよりコードを書く量が多いがその分カスタマイズ性があり、エラーが発生したときなど自分で書いたコードなのでエラーの特定がしやすい。
導入
sorcery wikiを参考にし、gemの導入からユーザー登録、ログイン、ログアウト機能を実装する。
実行環境
まずは、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>
これでユーザー登録、ログイン・ログアウト機能の完成。