善川の備忘録

自分のための備忘録ブログ

sorceryを導入しログイン機能を実装する

gem sorceryを用いることでログイン機能に関する便利なメソッドが利用出来るようになります。 ログイン, ログアウト機能を実装し使用できるメソッドを確認していきます。

github.com

新規Railsアプリを作成しsorceryを導入する

まずはRailsアプリを新規作成します。

rails new sorcery_app

sorceryを導入するためにGemfileに以下を追記

gem 'sorcery'

bundle installします。

bundle install --path vendor/bundle

以下のコマンドを入力しsorceryの必要なファイルを作成します。

rails g sorcery:install

マイグレーションファイルが作成されました。

# db/migrate/***_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :email,            null: false
      t.string :crypted_password
      t.string :salt

      t.timestamps                null: false
    end

    add_index :users, :email, unique: true
  end
end

マイグレーションしてUserモデルを作成します。

rails db:migrate

Userモデルのバリデーション

Userモデルのファイルが先程の「rails g sorcery:install」コマンドを入力した際に作成されています。 公式wikiを参考にバリデーションを追記します。

# app/models/user.rb
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] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: true
end

github.com

UsersController作成

scaffoldを使ってCRUD機能を備えたコントローラを作成します。

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
      invoke  resource_route
       route    resources :users
      invoke  test_unit
      create    test/controllers/users_controller_test.rb
      create    test/system/users_test.rb
      invoke  helper
      create    app/helpers/users_helper.rb
      invoke    test_unit
      invoke  jbuilder
      create    app/views/users/index.json.jbuilder
      create    app/views/users/show.json.jbuilder
      create    app/views/users/_user.json.jbuilder

railsサーバを立ち上げて試しに「localhost:3000/users」にアクセスするとusers#indexアクションが実行されていることが確認出来ます。

f:id:aftereffects:20210617185848p:plain

localhost:3000/users/new」にアクセスすると以下のフォームが表示されます。 f:id:aftereffects:20210617185407p:plain

「crypted_password」カラムには素のパスワードは入っておらず暗号化された文字列が入るようになっています。 sorceryでは「password」「password_confirmation」という仮想カラムが用意されておりフォームもこの2つを入力出来るように変更する必要があります。

# 以下暗号化されているかの確認
rails c
> user = User.new
> user.email = 'hoge@foo.bar'
> user.password = 'foobar'
> user.password_confirmation = 'foobar'
> user.save
> user.crypted_password
=> "$2a$10$pEn3qXUSh.cQ/UfdbalRReH2cpVHZS4KJAiqePTSiYLcAqXe8ltf6"

フォームを変更します。

# app/views/users/_form.html.erb
<%= form_with(model: user) do |form| %>
  <!-- 中略 -->

  <div class="field">
    <%= form.label :email %>
    <%= form.text_field :email %>
  </div>

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

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

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

UsersControllerのuser_paramsメソッドを変更します。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  #...

  private
    # ...

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

再度「localhost:3000/users/new」にアクセスし動作確認をしてみます。 f:id:aftereffects:20210617191500p:plain

Userモデルの作成が出来ました。 f:id:aftereffects:20210617191511p:plain

ログイン機能を作成

ログイン・ログアウト機能を作成していきます。 以下のコマンドを入力してUserSessionsControllerを作成します。

rails g controller UserSessions new create destroy
      create  app/controllers/user_sessions_controller.rb
       route  get 'user_sessions/new'
get 'user_sessions/create'
get 'user_sessions/destroy'
      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
      invoke  test_unit
      create    test/controllers/user_sessions_controller_test.rb
      invoke  helper
      create    app/helpers/user_sessions_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/user_sessions.scss

ログインフォームを作成します。

# app/views/user_sessions/new.html.erb
<h1>Login</h1>

<%= render 'form' %>
<%= link_to 'Back', users_path %>
# app/views/user_sessions/_form.html.erb
<%= form_with url: login_path, method: :post do |f| %>
  <div class="field">
    <%= f.label :email %>
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label :password %>
    <%= f.password_field :password %>
  </div>
  <div class="actions">
    <%= f.submit "Login" %>
  </div>
<% end %>

ルーティングを定義します。

# config/routes.rb
Rails.application.routes.draw do
  get 'login', to: 'user_sessions#new'
  post 'login', to: 'user_sessions#create'
  delete 'logout', to: 'user_sessions#destroy'
  resources :users
end

UserSessionsControllerのcreateアクションを作っていきます。

# app/controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
  def new; end

  def create
    # sorceryのloginメソッドを用いてuserを取得
    @user = login(params[:email], params[:password])
    if @user
      redirect_back_or_to users_path, notice: 'Login successful'
    else
      flash.now[:alert] = 'Login failed'
      render :new
    end
  end

  def destroy
  end
end

localhost:3000/login」にアクセスし動作確認を行います。 f:id:aftereffects:20210617193758p:plain

無事ログイン処理が作成出来ました!

f:id:aftereffects:20210617201815p:plain

現状だとログイン出来ているか分かりづらいのでヘッダーを追加してみます。 current_userメソッドは現在ログインしているユーザを取得出来ます。 logged_in?メソッドは現在ログインしているか真偽状態を確認出来ます。

# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <!-- 中略 -->
  </head>

  <body>
    <nav>
      <% if logged_in? %>
        現在のユーザ: <%= current_user.email %>
        <%= link_to 'ログアウト', logout_path %>
      <% else %>
        <%= link_to 'ログイン', login_path %>
      <% end %>
    </nav>
    <%= yield %>
  </body>
</html>

こんな感じになりました。

f:id:aftereffects:20210617201910p:plain

ログアウト機能を作成

UserSessionsControllerのdestroyアクションを作っていきます。 logoutは現在ログインしているセッションを削除する(= ログイン状態では無くす)メソッドになります。

# app/controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
  #...

  def destroy
    logout
    redirect_to users_path, notice: 'Logged out!'
  end
end

ログアウトのリンクをmethod: :deleteと定義しDELETEリクエストを送るよう変更する。

# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <!-- 中略 -->
  </head>

  <body>
    <nav>
      <% if logged_in? %>
        現在のユーザ: <%= current_user.email %>
        <%= link_to 'ログアウト', logout_path, method: :delete %>
      <% else %>
        <%= link_to 'ログイン', login_path %>
      <% end %>
    </nav>
    <%= yield %>
  </body>
</html>

ではログアウトリンクをクリックしてみます。 無事ログアウト処理が完了しました。

f:id:aftereffects:20210617201950p:plain

ログイン状態によりアクセス制御をする

例えば今回の例ではUsersControllerのedit, destroyアクションはログイン状態じゃないと実行出来ないようにしたいとする。

まず、ApplicationControllerにログイン状態じゃなかった時のリダイレクト先を定義する。 メソッド名はsorceryで指定のものがあるため以下と同じように記載すること。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base

  private
  def not_authenticated
    redirect_to login_path, alert: "Please login first"
  end
end

次にアクセス制御したいアクションに対しrequire_loginメソッドをbefore_actionで実行するよう定義する。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :require_login, only: %i[edit destroy]
  #...
end

以上でログインしていない状態で指定のアクションを実行するとリダイレクトするようになりました。

f:id:aftereffects:20210617202032p:plain

使用出来るメソッド

sorceryのAPI情報は公式ドキュメントに記載されています。

github.com

# README.md API Summary Coreより引用
require_login # This is a before action
login(email, password, remember_me = false)
auto_login(user) # Login without credentials
logout
logged_in? # Available in views
current_user # Available in views
redirect_back_or_to # Use when a user tries to access a page while logged out, is asked to login, and we want to return him back to the page he originally wanted
@user.external? # Users who signed up using Facebook, Twitter, etc.
@user.active_for_authentication? # Add this method to define behaviour that will prevent selected users from signing in
@user.valid_password?('secret') # Compares 'secret' with the actual user's password, returns true if they match
User.authenticates_with_sorcery!