LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 3 years have passed since last update.

Ruby on RailsでCRUD操作が出来るタスク管理アプリケーション構築 Part3

Last updated at Posted at 2019-12-16

Part2で構築したRailsアプリケーションにログイン+ログアウト機能を実装.

■ セッションとCookie.

ログイン機能実装に必要な前提知識であるセッション(Session)とクッキー(Cookie)の話.

■ セッション(Session)

Web上でHTTPリクエストのやりとりを行う場合、HTTPはステートレスなので状態管理が出来ない.

ユーザー間での状態管理を行うため、Webアプリケーションではセッションという仕組みを用意し、1つのブラウザから連続して送られる一連のHTTPリクエストの間で「状態」を共有できるようにしている.


# sessionメソッドを呼び出すことでアクセス可能
session[:user_id] = @user_id
# 値を取り出す方法
@user_id = session[:user_id]

■ クッキー(Cookie)

セッションと似た仕組みにクッキーがあり、セッションがアプリケーションサーバ側で独自に作られた仕組みに対し、クッキーはブラウザとWebサーバ間でやりとりされる汎用的な仕組み.

  1. WebサーバからブラウザへHTTPレスポンス返却時、何らかのクッキー情報(キーと値のペア)を送信.
  2. ブラウザ側ではクッキー情報をサーバのドメイン情報などと紐付けて保存.
  3. ブラウザは同一ドメインへHTTPリクエスト送信時、保管しているクッキー情報を添えて送信.

クッキー情報は複数リクエスト間で共有したい「状態」をブラウザ側に保存する仕組み.

Railsではセッションの仕組みの一部がクッキーによって実現されており、クッキーによってやり取りされるセッションIDをキーにして保管し、デフォルトでセッションIDの保管場所にクッキーを利用している(ブラウザ側で対応するクッキーを削除するとセッションはリセットされる).

■ Userモデル作成.

認証機能の実現のためにUserモデル(名前/メールアドレス/パスワード)を作成.

# Userモデル作成.
# Rails標準のhas_secure_passwordを使う場合、命名ルールが「XXXX_digest」となる.
# パスワードはハッシュ化されるので一方向性である.
bin/rails g model user name:string email:string password_digest:string

[ 実行結果 ]
Running via Spring preloader in process 2699
      invoke  active_record
      create    db/migrate/20191223043229_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

作成されたマイグレーションファイルを一部書き換え.

db/migrate/yyyymmddhhmmss_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.string :password_digest, null: false

      t.timestamps
      t.index :email, unique: true
    end
  end
end

マイグレーション実行.

bin/rails db:migrate

パスワードをdigestに変換し、保存できるようGemlfile(修正後にbundle実行)とモデル修正.

# [ 変更前 ]
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# [ 変更後 ]
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'

has_secure_passwordを記述すると、2つの属性が追加される.

app/models/users.rb
# password属性:ユーザーが入力した生のパスワードを一時的に格納するカラム.
# password_confirmation:パスワードの確認用.
class User < ApplicationRecord
  has_secure_password
end

コンソール上でUserオブジェクトを作成後にsave実行し、パスワードがハッシュ化されていることを確認.

User.new(name: 'ユーザー', email: 'sample@example.com', password: 'password', password_confirmation: 'password').save

内部的には画面上でpasswordとpassword_confirmation(確認用)を入力すると、内部(Userモデル)で一致しているか検証が行われ、一致している場合にはpassword_digestとしてハッシュ化されたパスワードが生成され、データベースのレコードに登録される.

■ ユーザー管理のためのDB.

管理者がユーザー登録可能な仕組みを作るため、Userモデルにadminフラグを追加.

# マイグレーションの雛形作成.
bin/rails g migration add_admin_to_users

作成されたマイグレーションファイルを書き換え.

db/migrate/yyyymmddhhmmss_add_admin_to_users.rb
# Usersテーブルにadminフラグを付与し、デフォルトfalse、nullは許可させない
class AddAdminToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :admin, :boolean, default: false, null: false
  end
end

マイグレーション実行.

bin/rails db:migrate

■ ユーザー管理のためのコントローラ.

ユーザー管理用のRailsコントローラクラスを作成.

# Railsではモジュール階層をコード保存するディレクトリ階層に対応させている.
# そのためapp/controllers/admin/users_controller.rbというファイルに対応.
bin/rails g controller Admin::Users new edit show index

ルーティングの修正.

app/config/routes.rb
# [ 変更前 ]
namespace :admin do
  get 'users/new'
  get 'users/edit'
  get 'users/show'
  get 'users/index'
end

# [ 変更後 ]
namespace :admin do
  resources: users
end

■ 登録処理.

ユーザー登録処理を実装.

app/controllers/admin/users_controllers.rb
class Admin::UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to admin_users_path, notice: "ユーザー「#{@user.name}」を登録しました。"
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :admin, :password, :password_confirmation)
  end
end
app/views/admin/users/new.html.slim
h1 ユーザー登録

.nav.justify-content-end
  = link_to '一覧', admin_users_path, class: 'nav-link'

= render partial: 'form', locals: { user: @user }

一部処理は共通利用されるため、部分テンプレート化.

app/views/admin/users/_form.html.slim
- if user.errors.present?
  ul#error_explanation
    - user.errors.full_messages.each do |message|
      li = message

= form_with model: [:admin, user], local: true do |f|
  .form-group
    = f.label :name, '名前'
    = f.text_field :name, class: 'form-control'
  .form-group
    = f.label :email, 'メールアドレス'
    = f.text_field :email, class: 'form-control'
  .form-check
    = f.label :admin, class: 'form-check-label' do
      = f.check_box :admin, class: 'form-check-input'
      | 管理者権限
  .form-group
    = f.label :password, 'パスワード'
    = f.password_field :password, class: 'form-control'
  .form-group
    = f.label :password_confirmation, 'パスワード(確認)'
    = f.password_field :password_confirmation, class: 'form-control'
  = f.submit '登録する', class: 'btn btn-primary'

不適切なデータの混入を避けるため、Userモデルにてバリデーション処理を実装.

app/models/user.rb
class User < ApplicationRecord
  has_secure_password

  # 名前が空の時はエラー
  validates :name, presence: true
  # メールアドレスが空、もしくはDB上で重複している場合はエラー
  validates :email, presence: true, uniqueness: true
end

■ その他CRUD.

ユーザー登録以外のCRUD処理を実装.

app/controllers/admin/users_controller.rb
class Admin::UsersController < ApplicationController
  def index
    @users = User.all
  end

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

  def new
    @user = User.new
  end

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

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to admin_users_path, notice: "ユーザー「#{@user.name}」を登録しました。"
    else
      render :new
    end
  end

  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      redirect_to admin_user_path(@user), notice: "ユーザー「#{@user.name}」を更新しました。"
    else
      render :new
    end
  end

  def destroy
    @user = User.find(params[:id])
    @user.destroy
    redirect_to admin_users_url, notice: "ユーザー「#{@user.name}」を削除しました。"
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :admin, :password, :password_confirmation)
  end
end
app/views/admin/users/index.html.slim
h1 ユーザー一覧

= link_to '新規登録', new_admin_user_path, class: 'btn btn-primary'

.mb-3
table.table.table-hover
  thead.thead-default
    tr
      th= User.human_attribute_name(:name)
      th= User.human_attribute_name(:email)
      th= User.human_attribute_name(:admin)
      th= User.human_attribute_name(:created_at)
      th= User.human_attribute_name(:updated_at)
      th
  tbody
    - @users.each do |user|
      tr
        td= link_to user.name, [:admin, user]
        td= user.email
        td= user.admin? ? 'あり' : 'なし'
        td= user.created_at
        td= user.updated_at
        td
          = link_to '編集', edit_admin_user_path(user), class: 'btn btn-primary mr-3'
          = link_to '削除', [:admin, user], method: :delete, data: { confirm: "ユーザー「#{user.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'
app/views/admin/users/edit.html.slim
h1 ユーザーの編集

.nav.justify-content-end
  = link_to '一覧', admin_users_path, class: 'nav-link'

= render partial: 'form', locals: { user: @user }
app/views/admin/users/show.html.slim
h1 ユーザーの詳細

.nav.justify-content-end
  = link_to '一覧', admin_users_path, class: 'nav-link'
table.table.table-hover
  tbody
    tr
      th= User.human_attribute_name(:id)
      td= @user.id
    tr
      th= User.human_attribute_name(:name)
      td= @user.name
    tr
      th= User.human_attribute_name(:email)
      td= @user.email
    tr
      th= User.human_attribute_name(:admin)
      td= @user.admin? ? 'あり' : 'なし'
    tr
      th= User.human_attribute_name(:created_at)
      td= @user.created_at
    tr
      th= User.human_attribute_name(:updated_at)
      td= @user.updated_at

= link_to '編集', edit_admin_user_path, class: 'btn btn-primary mr-3'
= link_to '削除', [:admin, @user], method: :delete, data: { confirm: "ユーザー「#{@user.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'
config/locales/ja.yml
ja:
  activerecord:
    ...
    models:
      task: タスク
    attributes:
      task:
      ...
      user:
        name: 名前
        email: メールアドレス
        admin: 管理者権限
        password: パスワード
        created_at: 登録日時
        updated_at: 更新日時

■ ログインフォーム作成.

ユーザーがログインするためのフォーム画面表示と、連携された情報を元に認証し、ログアウトも提供.

セッション管理のためのコントローラとビューを作成.

bin/rails g controller Session new

ログインフォーム表示のアクションURLを/loginにするため、routes.rbの書き換え.

config/routes.rb
Rails.application.routes.draw do
  get '/login', to: 'session#new'
  ...
end
app/views/sessions/new.html.slim
h1 ログイン

= form_with scope: :session, local: true do |f|
  .form-group
    = f.label :email, 'メールアドレス'
    = f.text_field :email, class: 'form-control', id: 'session_email'
  .form-group
    = f.label :password, 'パスワード'
    = f.password_field :password, class: 'form-control', id: 'session_password'
  = f.submit 'ログインする', class: 'btn btn-primary'

■ ログイン実行.

メールアドレスとパスワードを使ったログイン処理の実装.

config/routes.rb
Rails.application.routes.draw do
  get '/login', to: 'session#new'
  post '/login', to: 'session#create'
  ...
end
app/controllers/session_controller.rb
class SessionController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: session_params[:email])

    # 対象ユーザーが存在する、かつ認証成功の場合はログイン処理実行.
    if user&.authenticate(session_params[:password])
      # 認証成功時にはセッションにユーザーIDを設定.
      session[:user_id] = user.id
      redirect_to root_path, notice: 'ログインしました。'
    else
      render :new
    end
  end

  private

  # リクエストパラメータから必要な情報のみ抽出.
  def session_params
    params.require(:session).permit(:email, :password)
  end
end

■ セッション情報の共有化.

ログインに成功すると、以下のようにセッションからユーザー情報を簡単に取得できる.

User.find_by(id: session[:user_id])

頻繁に利用するので、AplicationControllerで取得可能なように制御.

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # 全てのビューで利用可能になる.
  helper_method :current_user

  private

  def current_user
    # 「||=」はnilガード -> @current_userが存在すれば@current_userを仮になければ右辺代入.
    @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
  end
end

ログインに成功すればcurrent_userにセッションIDが格納される.

■ ログアウト実装.

ログアウトはsession[:user_id]にnilが入っている状態になれば良い.

# セッションからuser_idをピンポイントで削除.
# ユーザーに紐づくその他のセッション情報も削除する場合はreset_sessionを利用.
session.delete(:user_id)

# セッション内の全ての情報を削除.
reset_session
config/routes.rb
Rails.application.routes.draw do
  get '/login', to: 'session#new'
  post '/login', to: 'session#create'
  delete '/logout', to: 'session#destroy'
  ...
end
app/controllers/sessions_controller.rb
class SessionController < ApplicationController
  ...

  def destroy
    reset_session
    redirect_to root_path, notice: 'ログアウトしました。'
  end

  private
  ...
end

先程定義したヘルパーメソッドを利用し、ログアウトしているか判定.

app/views/layouts/application.html.slim
doctype html
html
...
  body
    .app-title.navbar-expamd-md.navbar-light.bg-light
      .navbar-brand Taskleaf

    ul.navbar-nav.ml-auto
      - if current_user
        li.nav-item= link_to 'タスク一覧', tasks_path, class: 'nav-link'
        li.nav-item= link_to 'ユーザー一覧', admin_users_path, class: 'nav-link'
        li.nav-item= link_to 'ログアウト', logout_path, method: :delete, class: 'nav-link'
      - else
        li.nav-item= link_to 'ログイン', login_path, class: 'nav-link'
    .container
      - if flash.notice.present?
        .alert.alert-success = flash.notice
      = yield

■ 参考文献.

:books: 現場で使える Ruby on Rails 5速習実践ガイド.

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up