はじめに
今回から、以前作ったタスク管理アプリケーションにログイン機能を何回かに分けて実装していきます。
現段階のタスク管理アプリは、誰もが自由にタスクを追加、編集、削除できます。
しかし、このままでは自分のプライベートなタスクを他の人に見られたり、削除されてします恐れがあります。
このような事態を防ぐためには、認証機能を実装する必要があります。
アプリにユーザーが具体的に誰なのか把握させ、ユーザーが扱えるタスクに制限をかけます。
認証には、IDとパスワードを使うものや、外部サービスを使うものなど様々な方法があります。
タスク管理アプリにはメールアドレスとパスワードの組み合わせによる認証を実装します。
第一回となる今回は、以下の内容をまとめます。
- ・セッションとCookie
- ・Userモデルの作成
- ・パスワードのHash化
- ・ユーザー管理機能の実装
セッションとCookie
ログイン機能を実装する前に、セッションとCookieについて簡単に理解します。
セッション
WebアプリケーションではブラウザからサーバへHTTPリクエストを送り、HTTPレスポンスを受け取り画面として表示します。
このHTTPリクエストを繰り返し行うことで、順番に操作を行なっていきます。
しかしHTTPはステートレスなプロトコルであり、同じユーザから送られた一つ目のリクエスト二つ目のリクエストに情報を引き継げません。
一つのブラウザから連続して送られる一連のリクエストの中で状態を共有するサーバ側の仕組みを「セッション」と言います。
Railsではコントローラからsessionメソッドを呼び出すことで、セッションにアクセスできます。
セッションにデータを入れるには任意のキーを指定して値を格納します。
session[:user_id] = @user_id
値は以下のように取り出します。
@user_id = session[:user_id]
Cookie
セッションと似た仕組みとしてCookieがあります。セッションがアプリケーションサーバで独自に実現される仕組みであるのに対して、Cookieはブラウザとサーバ間でやり取りされる仕組みです。
Cookieでは、まずWebサーバからブラウザへHTTPレスポンスを返す際、何らかのCookie情報を含めて送ります。ブラウザはCookie情報をサーバのドメインなどの情報に紐付けて保存します。
次に同じドメインに対してHTTPリクエストを送る際に、保管していたCookie情報を添えて送ります。これによりWebサーバは以前どのようなCookie情報を受け取ったブラウザからリクエストが送られて来たのかを把握できます。
このような複数のリクエスト間で共有したい「状態」をブラウザ側に保存する仕組みをCookieといいます。
Railsでは、cookiesメソッドでCookie情報にアクセスし、データに対して操作を行えますが、基本的にはセッションを使えば済むので、直接Cookieを操作することはあまりありません。
RailsにおけるセッションとCookieの関係
RailsではセッションはCookieによってやりとりされるセッションIDをキーにして保管されます。
セッションデータの保管場所は複数の選択肢から選べますが、デフォルトではCookieとなっています。
このようにRailsのセッションの仕組みの一部はCookieによって実現されています。ブラウザ側で対応するCookieを消すとセッションもリセットされます。
Userモデルの作成
認証機能を実装するには、タスク管理アプリを利用するユーザーという概念を加えます。そこでユーザーを表すUserモデルを作成します。
Userモデルのデータ構造
Userクラスのデータ構造は以下のようにします。
- 属性名:name データ型:string(文字列)
- 属性名:email データ型:string(文字列)
- 属性名:password_digest データ型:string(文字列)
パスワードの属性名をpassword_digestとしています。
次節でパスワードをHash化する際にRailsに標準で付いているhas_secure_passwordという機能を使います。
password_digestはこの機能の命名ルールに沿った名前となっています。
Userモデルの作成
以下のコマンドを実行してUserモデルとuserテーブルを作るためのマイグレーションファイルを作成します。
$ bin/rails g model user name:string email:string password_digest:string
Running via Spring preloader in process 79327
invoke active_record
create db/migrate/20210321172502_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[6.1]
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
これで名前、メールアドレス、パスワードに何かしらの文字列が入り、同じメールアドレスを持つユーザーが複数存在することはなくなりました。
編集できたらマイグレーションを実行してusersテーブルを作成します。
$ bin/rails db:migrate
== 20210321172502 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0281s
== 20210321172502 CreateUsers: migrated (0.0281s) =============================
これでusersテーブルを作成できました。
パスワードのHash化
現段階ではユーザーの名前やメールアドレスをデータベースに保存する仕組みは出来上がっておりますが、パスワードをdigestに変換する仕組みはありません。
digestとは、元の値に戻すことのできない一方的なHash化を行なった文字列のことです。
digest自体は無意味に見える文字列であり、同じパスワードから生成すると常に同じ値になります。逆変換はできないため、digestだけを保存するようにすれば、データベースの値が漏洩してもパスワード自体が漏洩する事態には陥りません。
そこでパスワードの入力を受け付けてdigestを生成・保存するようモデルに実装します。
ここでhas_secure_passwordを使います。has_secure_passwordを使ってパスワードをハッシュ化するためにbcryptというハッシュ関数を提供するgemが必要になります。
Gemfileから以下の記述のコメントアウトを外して保存します。
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7'
bundleコマンドを実行します。
$ bundle
これでbcryptをインストールできたので、has_secure_passwordを使えます。
Userモデルに以下のように変更します。
class User < ApplicationRecord
has_secure_password
end
has_secure_passwordを記述するとデータベースのカラムには対応しない以下の二つの属性が追加されます。
- ・属性名:password 役割:ユーザーが入力した生のパスワードを一時的に格納
- ・属性名:password_confirmation 役割:ユーザー登録時に確認用パスワードを一時的に格納
password属性とpassword_confirmation属性の値が一致しない場合、検証は失敗します。
ユーザー管理機能の実装
アプリケーションの利用者をどのようにデータベースに登録するかですが、今回は管理者がユーザを登録するタイプのユーザー管理機能を実装します。
adminフラグの追加
このユーザー管理機能は、/adminで始まるURLで提供し、adminというフラグがtrueのユーザー(管理者)だけが利用できるようにします。
$ bin/rails g migration add_admin_to_users
Running via Spring preloader in process 80529
invoke active_record
create db/migrate/20210321204250_add_admin_to_users.rb
生成したマイグレーションファイルを以下の通りに編集します。
usersテーブルにadminカラムを追加します。
/db/migrate/20210321204250_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :admin, :boolean, default: false, null: false
end
end
編集したら以下のコマンドを実行します。
$ bin/rails db:migrate
コントローラの実装
CRUD機能を持ったAdmin::UsersControllerという名前のコントローラを作成します。
Admin::UsersControllerには以下のアクションを持たせます。

以下のコマンドを実行してapp/controllers/admin/users_controller.rbを画面を伴うGETアクションのビューとともに作成します。
$ bin/rails g controller Admin::Users new edit show index
Running via Spring preloader in process 81279
create app/controllers/admin/users_controller.rb
route namespace :admin do
get 'users/new'
get 'users/edit'
get 'users/show'
get 'users/index'
end
invoke slim
create app/views/admin/users
create app/views/admin/users/new.html.slim
create app/views/admin/users/edit.html.slim
create app/views/admin/users/show.html.slim
create app/views/admin/users/index.html.slim
invoke test_unit
create test/controllers/admin/users_controller_test.rb
invoke helper
create app/helpers/admin/users_helper.rb
invoke test_unit
invoke assets
invoke scss
create app/assets/stylesheets/admin/users.scss
new、edit、show、indexのからのアクションとデフォルトのビューが用意できました。
次に、想定したURLで適切なアクションにリクエストが飛ぶようにroute.rbを以下のように編集します。
config/route.rb
Rails.application.routes.draw do
namespace :admin do
resources :users
end
resources :tasks
root to: 'tasks#index'
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
各機能のURLを取得するにはadmin_users_pathやadmin_user_pathのようにadmin_のついたヘルパーメソッドを使います。
登録機能の実装
登録機能を実装します。コントローラ(app/controllers/admin/users_controller.rb)とビュー(app/views/admin/users/new.html.slim)を以下の通りに編集します。
app/controllers/admin/users_controller.rb
class Admin::UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
redirect_to admin_user_url(@user), 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 ユーザー登録
= 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
validates :email, presence: true, uniqueness: true
end
名前とメールアドレスが空のときやメールアドレスが重複するときは登録できないようにできました。
サーバを再起動し、/admin/users/newにアクセスすると以下の画面が表示されます。

その他機能の実装
残りの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_user_url(@user), notice: "ユーザー「#(@user.name)」を登録しました。"
else
render :new
end
end
def update
@user = User.find(params[:id])
if @user.update(user_params)
redirect_to admin_user_url(@user), notice: "ユーザー「#(@user.name)」を更新しました。"
else
render :edit
end
end
def destroy
@user = User.find(params[:id])
@user.destroy
redirect_to admin_user_url(@user), 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
thread.thread-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/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/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/_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'
app/views/admin/users/show.html.slim
h1 ユーザーの詳細
.nav.justify-content-end
= link_to '一覧', admin_users_path, class: 'nav-link'
table.tabel.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'
以上でユーザー管理機能の基礎ができました。
/admin/usersにアクセスすることで機能を利用することができます。
この機能へのリンクはのちにレイアウトに追加します。
今回はここまでです・
終わりに
タスク管理アプリケーションにログイン機能を追加する準備としてユーザー管理機能の基礎部分を実装しました。
しかしこのままでは管理者でなくてもユーザー管理機能を使えてしまいます。ログインしている管理者のみ使えるように制限をかける必要がありますが、そのためには先にログイン機能を実装する必要があります。
次回はログイン機能とログアウト機能を実装します。
コメント