■ 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サーバ間でやりとりされる汎用的な仕組み.
- WebサーバからブラウザへHTTPレスポンス返却時、何らかのクッキー情報(キーと値のペア)を送信.
- ブラウザ側ではクッキー情報をサーバのドメイン情報などと紐付けて保存.
- ブラウザは同一ドメインへ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
作成されたマイグレーションファイルを一部書き換え.
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つの属性が追加される.
# 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
作成されたマイグレーションファイルを書き換え.
# 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
ルーティングの修正.
# [ 変更前 ]
namespace :admin do
get 'users/new'
get 'users/edit'
get 'users/show'
get 'users/index'
end
# [ 変更後 ]
namespace :admin do
resources: users
end
■ 登録処理.
ユーザー登録処理を実装.
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
h1 ユーザー登録
.nav.justify-content-end
= link_to '一覧', admin_users_path, class: 'nav-link'
= render partial: 'form', locals: { user: @user }
一部処理は共通利用されるため、部分テンプレート化.
- 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モデルにてバリデーション処理を実装.
class User < ApplicationRecord
has_secure_password
# 名前が空の時はエラー
validates :name, presence: true
# メールアドレスが空、もしくはDB上で重複している場合はエラー
validates :email, presence: true, uniqueness: true
end
■ その他CRUD.
ユーザー登録以外のCRUD処理を実装.
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
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'
h1 ユーザーの編集
.nav.justify-content-end
= link_to '一覧', admin_users_path, class: 'nav-link'
= render partial: 'form', locals: { user: @user }
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'
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の書き換え.
Rails.application.routes.draw do
get '/login', to: 'session#new'
...
end
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'
■ ログイン実行.
メールアドレスとパスワードを使ったログイン処理の実装.
Rails.application.routes.draw do
get '/login', to: 'session#new'
post '/login', to: 'session#create'
...
end
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で取得可能なように制御.
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
Rails.application.routes.draw do
get '/login', to: 'session#new'
post '/login', to: 'session#create'
delete '/logout', to: 'session#destroy'
...
end
class SessionController < ApplicationController
...
def destroy
reset_session
redirect_to root_path, notice: 'ログアウトしました。'
end
private
...
end
先程定義したヘルパーメソッドを利用し、ログアウトしているか判定.
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
■ 参考文献.
現場で使える Ruby on Rails 5速習実践ガイド.