gem 'sorcery' とは
gem 'sorcery' は、Railsにおける認証システムの実装を支援するライブラリ。ログイン、ログアウト、セッション管理などの基本的な認証機能を簡単に追加できる。
今回はそんな'sorcery'を使って、ユーザー機能の実装を行なってみよう!
gem 'sorcery'導入
公式に導入方法が詳しく載っているので割愛します。
実装概要
ゴールへ向けた大まかな実装手順を提示します。
今回の課題で必要なタスク
- ユーザー登録機能
- ログイン機能
- ログアウト機能
- gem 'sorcery'を使用
そこから明確にイメージできるものとそうでないものを区別しよう!
- イメージできるもの
- ユーザー登録
- ログイン
- ログアウト
- イメージできないもの
- gem 'sorcery'
ゴールまでの流れで実装に必要なものは?
- ユーザー登録
-
ユーザー方法を登録するためにテーブル・カラムを用意する
-
ユーザー登録処理に関わるルーティングを用意する
ユーザー登録画面を表示するもの
ユーザー登録処理を行うもの
-
用意したルーティングに対応するコントローラ・アクションを用意する
-
ユーザー登録画面を用意する
-
- ログイン
- ログイン処理に関わるルーティングを用意する
ログイン画面を表示するもの
ログイン処理を行うもの - 用意したルーティングに対応するコントローラ・アクションを用意する
- ログイン画面を用意する
- ログイン処理に関わるルーティングを用意する
- ログアウト
- ログアウト処理に関わるルーティングを用意する
ログアウト処理を行うもの - 用意したルーティングに対応するコントローラ・アクションを用意する
- ログアウトを行うリンク・ボタンを用意する
- ログアウト処理に関わるルーティングを用意する
イメージできなかったものについては調べよう!
実装フェーズ
※docker環境で作業する想定です。
実装前に以下の項目を立ち上げてください。なおDocker環境構築については割愛します。
- dockerアプリ
- docker compose up
- docker compose exec web bin/dev
■Sorcery導入
Gemfileに gem 'sorcery', '0.16.3' を記述する
docker compose run web bundle install コマンドを実行する
このコマンドを使用すると、Gemfileに記述した変更をインストールします。
docker compose run web rails g sorcery:install コマンドを実行する
このコマンドを使用すると、sorceryの設定を管理する初期設定ファイルやユーザー認証に必要なデータベースを作成するマイグレーションファイルが生成されます。
docker compose restart コマンドを実行する(docker compose exec web bin/dev コマンドも別途実行する)
Dockerを再起動して、データベースも再度立ち上げます。
■ユーザー登録
マイグレーションファイル
docker compose run web rails g sorcery:install コマンドで生成されたマイグレーションファイルを編集する
マイグレーションファイルとは、データの変更を簡単に行うためのもので、新しいテーブルの作成やカラムの変更が含まれます。
class SorceryCore < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :email, null: false, index: { unique: true }
t.string :crypted_password
t.string :salt
t.string :first_name, null: false
t.string :last_name, null: false
t.timestamps null: false
end
end
end
docker compose exec web rails db:migrate コマンドを実行する
マイグレーションファイルを更新するためのもの。
docker compose exec web rails db:migrate:status コマンドで確認する
ゼンマイグレー村ファイルの適用状況が(up/down)確認できる。
モデル
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 :first_name, presence: true, length: { maximum: 255 }
validates :last_name, presence: true, length: { maximum: 255 }
validates :email, presence: true, uniqueness: true
end
- password;最小で3文字以上必要(新規レコード作成もしくはcrypted_passwordカラムが更新される時のみ適応)
- password_confirmation:値が空でないこと・passwordの値と一致すること(新規レコード作成もしくはcrypted_passwordカラムが更新される時のみ適応)
- first_name:値が空でないこと・最大255文字以下であること
- last_name:値が空でないこと・最大255文字以下であること
- email:値が空でないこと・ユニークな値であること
ルーティング
config/routes.rbにユーザー登録用のルーティングを記述する
Rails.application.routes.draw do
root "static_pages#top"
resources :users, only: %i[new create]end
●resources
ルートを一括で生成するメソッド。
例えば、resources :users と記述すると、ユーザーに関連する一連のルート(index, new, create, show, edit, update, destroy)が自動的に設定されます。
●only指定
resoourcesメソッドで生成されるルートの中から生成したいsクションだけを選択できます。
例えば、resources :users, only: [:index] と指定すると、ユーザー一覧だけが生成されます。
コントローラー
app/controllers/application_controller.rb を編集する
class ApplicationController < ActionController::Base
before_action :require_login
private
def not_authenticated
redirect_to login_path
end
end
●require_login
ユーザーがログインしているかどうか判断する。ログインしていない場合は、not_authenticated メソッドに返されます。
●before_action
ApplicationController クラスを継承している全てのコントローラーで適用される。
●not_authenticated
ログインしていない場合に指定されたパスにリダイレクトします。
app/controllers/static_pages_controller.rb を編集する
class StaticPagesController < ApplicationController
skip_before_action :require_login, only: %i[top]
def top; end
end
app/controllers/users_controller.rb を生成し、new, createアクションを定義する
$ docker compose exec web rails g controller users
class UsersController < ApplicationController
skip_before_action :require_login, only: %i[new create]
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
redirect_to root_path
else
render :new
end
end
private
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation)
end
end
●ストロングパラメーター
Railsでデータを更新する際に許可されたもののみを受け入れて、不正なデータは弾く仕組みです。
例えば、user_paramsメソッドでは、permitメソッドで許可されたものを指定します。記載のない(age,addressなど)は弾かれます。
●skip_before_action
特定のアクションが実行される前に指定したフィルターをスキップするために使用される。
今回は、require_login というフィルターを new および create アクションに対してスキップしています。
ビュー
app/views/users/配下に以下のファイルを生成・記述する
<div class="container">
<div class="row">
<div class="col-md-10 col-lg-8 mx-auto">
<h1>ユーザー登録</h1>
<%= form_with model: @user do |f| %>
<div class="mb-3">
<%= f.label :last_name, class: "form-label" %>
<%= f.text_field :last_name, class: "form-control" %>
</div>
<div class="mb-3">
<%= f.label :first_name, class: "form-label" %>
<%= f.text_field :first_name, class: "form-control" %>
</div>
<div class="mb-3">
<%= f.label :email, class: "form-label" %>
<%= f.email_field :email, class: "form-control" %>
</div>
<div class="mb-3">
<%= f.label :password, class: "form-label" %>
<%= f.password_field :password, class: "form-control" %>
</div>
<div class="mb-3">
<%= f.label :password_confirmation, class: "form-label" %>
<%= f.password_field :password_confirmation, class: "form-control" %>
</div>
<%= f.submit "登録", class: "btn btn-primary" %>
<% end %>
<div class='text-center'>
<%= link_to 'ログインページへ', login_path %>
</div>
</div>
</div>
</div>
●form_with
フォームを簡単に作成するヘルパーメソッドです。
model: @user のようにモデルを指定すると、適切なリクエスト先を自動で選択します。
例えば、@userが新規オブジェクトの場合、フォームはusers#create へのPOSTリクエストを行うといった具合です。
■ログイン
ルーティング
config/routes.rb にログイン用のルーティングを記述する
Rails.application.routes.draw do
root "static_pages#top"
resources :users, only: %i[new create]
get 'login', to: 'user_sessions#new'
post 'login', to: 'user_sessions#create'end
コントローラー
app/controllers/user_sessions_controller.rb を生成し、new, create, destroyアクションを定義する
$ docker compose exec web rails g controller user_sessions
class UserSessionsController < ApplicationController
skip_before_action :require_login, only: %i[new create]def new; end
def create@user = login(params[:email], params[:password])
if @user
redirect_to root_path
else
render :new
end
end
def destroy
logout
redirect_to root_path, status: :see_other
end
end
●redirect_to
ログインやログアウト後に使われ、指定されたURLへ返却します。
●render
ユーザーの入力エラーの時に使われ、現在のビューを表示し入力データも保持します。
ビュー
app/views/user_sessions/配下にnew.html.erbファイルを生成・記述する
<div class="container">
<div class="row">
<div class=" col-md-10 col-lg-8 mx-auto">
<h1>ログイン</h1>
<%= form_with url: login_path do |f| %>
<div class="mb-3">
<%= f.label :email %>
<%= f.email_field :email, class: "form-control" %>
</div>
<div class="mb-3">
<%= f.label :password %>
<%= f.password_field :password, class: "form-control" %>
</div>
<%= f.submit "ログイン", class: "btn btn-primary" %>
<% end %>
<div class='text-center'>
<%= link_to '登録ページへ', new_user_path %>
<%= link_to 'パスワードをお忘れの方はこちら', '#' %>
</div>
</div>
</div>
</div>
ログイン画面への導線(リンク)を用意する
app/views/shared/_header.html.erb
<header>
<nav class="navbar navbar-expand-lg navigation navbar-light bg-light">
<%= link_to root_path, class: "navbar-brand" do %>
<%= image_tag "logo.png" %>
<% end %>
<button class='navbar-toggler' data-bs-toggle='collapse' data-bs-target='#navbarSupportedContent' aria-controls='navbarSupportedContent' aria-expanded='false' aria-label='Toggle navigation'>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ms-auto main-nav align-items-center">
<li class="nav-item">
<%= link_to 'ログイン', login_path, class: "nav-link" %>
</li>
</ul>
</div>
</nav>
</header>
■ログアウト
ルーティング
config/routes.rbにログアウト用のルーティングを記述する
Rails.application.routes.draw do
root "static_pages#top"
resources :users, only: %i[new create]
get 'login', to: 'user_sessions#new'
post 'login', to: 'user_sessions#create'
delete 'logout', to: 'user_sessions#destroy'end
delete 'logout', to: 'user_sessions#destroy' は、Railsのルーティング設定でログアウト機能を実装するために使用される記述です。
ビュー
未ログイン・ログイン状態が判別できるようにヘッダーの表示を出し分ける
app/views/shared/_before_login_header.html.erb
<header>
<nav class="navbar navbar-expand-lg navigation navbar-light bg-light">
<%= link_to root_path, class: "navbar-brand" do %>
<%= image_tag "logo.png" %>
<% end %>
<button class='navbar-toggler' data-bs-toggle='collapse' data-bs-target='#navbarSupportedContent' aria-controls='navbarSupportedContent' aria-expanded='false' aria-label='Toggle navigation'>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ms-auto main-nav align-items-center">
<li class="nav-item">
<%= link_to 'ログイン', login_path, class: "nav-link" %>
</li>
</ul>
</div>
</nav>
</header>
app/views/shared/_header.html.erb
<header>
<nav class='navbar navbar-expand-lg navigation navbar-light bg-light'>
<%= link_to '/', class: 'navbar-brand' do %>
<%= image_tag 'logo.png' %>
<% end %>
<button class='navbar-toggler' data-bs-toggle='collapse' data-bs-target='#navbarSupportedContent'
aria-controls='navbarSupportedContent' aria-expanded='false' aria-label='Toggle navigation'>
<span class='navbar-toggler-icon'></span>
</button>
<div class='collapse navbar-collapse' id='navbarSupportedContent'>
<ul class='navbar-nav ms-auto main-nav align-items-center'>
<li class='nav-item dropdown dropdown-slide'>
<%= link_to '掲示板', '#', class: 'nav-link dropdown-toggle', data: { bs_toggle: 'dropdown' }, aria: { haspopup: 'true', expanded: 'false' }, id: 'header-board-menu' %>
<div class='dropdown-menu dropdown-menu-end'>
<%= link_to '掲示板一覧', '#', class: 'dropdown-item' %>
<%= link_to '掲示板作成', '#', class: 'dropdown-item' %>
</div>
</li>
<li class='nav-item'>
<%= link_to 'ブックマーク一覧', '#', class: 'nav-link' %>
</li>
<li class='nav-item dropdown dropdown-slide'>
<%= link_to '#', class: 'nav-link', data: { bs_toggle: 'dropdown' }, aria: { haspopup: 'true', expanded: 'false' }, id: 'header-profile' do %>
<%= image_tag 'sample.jpg', class: 'rounded-circle mr15', width: '40', height: '40' %>
<% end %>
<div class='dropdown-menu dropdown-menu-end'>
<div class='dropdown-item'>らんてっく たろう</div>
<div class='dropdown-divider'></div>
<%= link_to 'プロフィール', '#', class: 'dropdown-item' %>
<%= link_to 'ログアウト', '#', class: 'dropdown-item', data: { turbo_method: :delete } %>
</div>
</li>
</ul>
</div>
</nav>
</header>
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>RUNTEQ BOARD APP</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
</head>
<body>
<% if logged_in? %>
<%= render 'shared/header' %>
<% else %>
<%= render 'shared/before_login_header' %>
<% end %>
<%= yield %>
<%= render 'shared/footer' %>
</body>
</html>
●if logged_in?
ユーザーのログイン状態に応じてヘッダーを出し分けるもの。