【Rails入門説明書】sessionについて解説
はじめに
Railsは、Webサービスを作ることができるフレームワークです。
そして、Webサービスには、特定のユーザーを登録して、ログイン・ログアウトを行うことができるログイン機能は、不可欠と言っても良い機能です。
しかし、ログイン機能を実装するだけでは、「ログイン」という手間をユーザーにかけさせるだけで、ユーザビリティを著しく低下させるだけになってしまいます。
そのため、現在の多くのWebサイトやサービスでは、ログイン後はログアウトするか一定期間が経過するまで、ログインを行わなくても良いような機能が備わっています。
セッション(session)は、そんなログインというユーザーにとって手間のかかる機能を、できる限り使いやすくするために役立つしくみなのです。
今回は、そんなセッションについて、詳しく解説いたします。
セッション(session)とは
sessionというのは、前述の通りログインしたあと、ページ遷移しても再度ログインを行わなくても良いように、「ログインしている」ことを記憶しておく機能のことです。
そもそも、インターネットを支えているプロトコルであるHTTPは、ステートレスなものです。
ステートレスというのは、ステート(State:状態)レス(less:~がない)ですので、状態を記憶しておらず、ただリクエストに対するレスポンスだけで処理するというものです。
つまり、HTTPは、同じリクエストに対しては、常に同じレスポンスを返すというのが、基本的な動作なわけです。
インターネット黎明期であればそれで十分だったかもしれませんが、今ではステートレスなサイトの方が珍しく、ユーザーに合わせた動きをするのが、一般的でしょう。
そんな、「アクセスした人が過去に行った操作履歴」を「状態」として保持し、それに合わせてレスポンスを返すステートフル(Stateful)な動きを実現するためのしくみの1つがセッションなのです。
※なお、広義には、このしくみ全体をセッションと呼びます。しかし、狭義には、「状態を保持している」ということをセッションと呼び、「セッションを管理する」、「セッションを切る(消す)」などと言うこともあります
セッションには、ログインしたユーザーの「ID」と「ログイン履歴」が保持されています。そして、任意のタイミングでその記録を呼び出すことができるのです。
そのため、ユーザーがアクセスしたときにその記録を確認することで、「すでにログイン済み」であれば、ログイン処理をスキップさせるといったことを行うことができるようになります。
環境構築
セッションを詳しく理解していただくために、テスト環境を構築しましょう。
少し準備が面倒かもしれませんが、ログイン機能の学習にもなりますので、順に進めていってください。
基本的な環境構築
まずは、ベースとなる環境を作り、has_secure_passwordメソッドを使ってパスワードを暗号化できるようにします。
環境構築
コマンドプロンプトで、以下のコマンドを実行します。
[bash]rails new session_test
cd session_test
rails generate scaffold User name:string mail:string password_digest:string
rake db:migrate
[/bash]
ログインを行うので、名前(name)とメールアドレス(mail)、パスワード(password_digest)のカラムを作っています。
なお、パスワードを格納するカラム名は、必ず「password_digest」にしておいてください。(今回、パスワードの暗号化に利用する「has_secure_passwordメソッド」で決められているためです)
has_secure_passwordによるパスワードの暗号化
パスワードを扱う場合は、暗号化の処理が必須となります。
Rails4以降には、has_secure_passwordというパスワード暗号化機能が標準搭載されていますので、それを利用しましょう。
ただし、rails newで構築しただけでは、has_secure_passwordは導入されていませんので、Gemfileを修正してbundle installしなければいけません。
(Gemfile)
# Use ActiveModel has_secure_password
gem ‘bcrypt’, ‘~> 3.1.7’
「bcrypt」というgemがhas_secure_passwordメソッドを利用するためのgemですが、デフォルトではコメントアウトされていますので、有効化してbundle installします。
次に、モデルファイルを以下のように変更します。(2行追加)
(app/models/user.rb)
class User < ApplicationRecord
has_secure_password validations: true
validates :mail, presence: true, uniqueness: true
end
先ほど説明した「has_secure_passwordメソッド」をモデルに追記することで、password_digestカラムの値を暗号化し、かつ「password」と「password_confirmation」という2つのテーブルに存在しないプロパティが追加されます。
「has_secure_passwordメソッド」は、このpasswordプロパティの値を暗号化して、password_digestカラムへ保存してくれるわけです。
また、validationsオプションをtrueにしておくことで、パスワードを入力必須、かつ「password」と「password_confirmation」が一致していなければ登録できないようにしておきます。
あと、合わせて、メールアドレスを入力必須にして、かつ重複した登録ができないように、バリデーションを追加して、一般的なログインユーザー作成用のモデルは完成です。
※バリデーションについては「【Rails入門説明書】validationsについて解説(1)」を参照してください
このモデルを登録するように、フォームViewとコントローラを以下のように修正しておきます。
(app/views/users/_form.html.erb)
<%= form_with(model: user, local: true) do |form| %>
<% if user.errors.any? %>
<div id=”error_explanation”>
<h2><%= pluralize(user.errors.count, “error”) %> prohibited this user from being saved:</h2>
<ul>
<% user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class=”field”>
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div class=”field”>
<%= form.label :mail %>
<%= form.text_field :mail %>
</div>
<!– delete
<div class=”field”>
<%= form.label :password_digest %>
<%= form.text_field :password_digest %>
</div>
–>
<div class=”field”>
<%= form.label :password %>
<%= form.text_field :password %>
</div>
<div class=”field”>
<%= form.label :password_confirmation %>
<%= form.text_field :password_confirmation %>
</div>
<div class=”actions”>
<%= form.submit %>
</div>
<% end %>
※password_digestの入力フォームを削除して、代わりにpasswordとpassword_confirmationの入力フォームを追加しています
(app/controllers/users_controller.rb)
:
def user_params
# params.require(:user).permit(:name, :mail, :password_digest)
params.require(:user).permit(:name, :mail, :password, :password_confirmation)
end
end
※末尾にあるuser_paramsメソッドのみ修正します。修正内容はViewファイル同様、password_digestを削除して、代わりにpasswordとpassword_confirmationを追加します
※user_paramsメソッドについては、「【Rails入門説明書】paramsについて解説」を参照してください
念のため、サーバーを起動し、「localhost:3000/users/new」にアクセスして、動作確認してみましょう。
以下のようなフォームが表示され、ユーザー登録することができます。
なお、ユーザー登録後の一覧画面や詳細画面には、password_digestに暗号化されたパスワードが表示されています。
セッションを使って、ログイン・ログアウトの管理
ユーザー登録ができるようになりましたので、セッションを利用したログイン機能を実装しながら、詳細を説明していきましょう。
実装概要
ここでいきなりプログラムするのではなく、セッションを使ったログイン機能の大まかな概要を説明しておきましょう。
ログインしているということは、「正しいユーザーがアクセスしている」という状態です。
「状態」ですので、アクセスした人が「正しいユーザー名とパスワードの組み合わせ」を入力したという確認をするだけではなく、その結果を記録しておく必要があるわけです。
しかし、ステートレスなWebアプリケーションの場合、この結果をページ間で保持しません。
そのため、「正しいユーザー名とパスワードの組み合わせ」を入力したあとにページ遷移すると、改めて確認しなければ「正しいユーザーがアクセスしている」という状態かどうかが分からないのです。
ログイン機能を実現する重要なポイントは、この「状態の保持」であり、それにセッションを利用するということなのです。
今回は、セッションに保存するセッション情報(「正しいユーザーがアクセスしている」状態)として、メールアドレスを生成してブラウザのCookieへ保存します。
Cookieはブラウザが持っている作業用ファイルですので、そのファイルを確認することで、ログイン中なのかどうかを認識することができるようになるわけです。
そして、ログインしている状態でしかアクセスできないページへの遷移時に、セッション情報とテーブルのメールアドレスを比較することで、ログイン処理した正しいユーザーがアクセスしている状態かどうかを確認します。
また、ログアウト時にセッション情報を削除しておけば、上述の一致チェックが成り立ちませんので、ログイン処理を促すきっかけ(トリガ)になるのです。
なお、ログインしている状態でしかアクセスできないページは、簡略化のために、すべてのページにします。
以上が、これから実装していくログイン機能の概要です。まとめると、次のようになります。
追加するファイル/処理
・セッション管理用のコントローラ
・ログイン画面(セッション追加)のためのView
・ルーティング
ログインが必要なページ
・原則としてすべて(ログイン画面など、不要な画面は要確認)
ログイン処理
・正しいユーザー名とパスワードを入力したかどうかの確認
・メールアドレスをセッションへ格納
ログアウト処理
・ログアウトはどこでもできるようにする
・セッションを空にする
ログイン状態の確認
・ログインが必要なページへのアクセスで、テーブルとセッションのメールアドレスが一致しているかのチェックを行う
・ログインしていなければ、ログイン画面へ遷移
ファイルとルーティングの追加
では早速、ログイン機能の追加を行っていきましょう。
まずは、ベースとなるセッション管理用のファイルとルーティングを追加します。まずは、次のコマンドでコントローラとViewを追加します。
[bash]rails generate controller sessions new create destroy
[/bash]
これで必要なファイルが生成されましたが、ルーティングが、それぞれのアクションを実行する(GETリクエストで画面表示する)だけになっています。
(config/routes.rb)
Rails.application.routes.draw do
get ‘sessions/new’
get ‘sessions/create’
get ‘sessions/destroy’
resources :users
end
そのため、以下のように修正します。
(config/routes.rb)
Rails.application.routes.draw do
get ‘login’, to: ‘sessions#new’
post ‘login’, to: ‘sessions#create’
get ‘logout’, to: ‘sessions#destroy’
resources :users
end
これで、「/login」へのアクセスでsessions#newアクション、ログイン画面のフォーム入力確定でsessions#createアクション、ログアウト時にsessions#destroyアクションが実行されるようになりました。
次にViewファイルを修正します。
Viewファイルは「app/views/sessions」ディレクトリに3つ生成されていますが、画面が必要なのはログイン画面だけですので、必要なのは「new.html.erb」だけです。
自動生成されたものは、タイトルと簡単な説明の表示だけで、何もしないようになっています。
(app/views/sessions/new.html.erb)
<h1>Sessions#new</h1>
<p>Find me in app/views/sessions/new.html.erb</p>
そのため、以下のように修正してください。
(app/views/sessions/new.html.erb)
<h1>LOGIN</h1>
<%= form_with(url: login_path, local: true) do |form| %>
<div class=”field”>
<%= form.label :mail %>
<%= form.text_field :mail %>
</div>
<div class=”field”>
<%= form.label :password %>
<%= form.password_field :password %>
</div>
<div class=”actions”>
<%= form.submit ‘login’%>
</div>
<% end %>
試しに、「localhost:3000/login」にアクセスしてみると、以下のように表示されれば、Viewファイルとルーティングはうまくいっています。
また、一覧画面にログインしているメールアドレスの表示と、ログアウトするためのリンクを追加します。(他の画面にも追加した方がユーザービリティは高くなりますが、ここでは一覧画面にだけ追加します)
(app/views/sessions/index.html.erb)
:
<%= link_to ‘New User’, new_user_path %>
<p><%= session[:user_mail] %>/ [<%= link_to ‘logout’, logout_path %>]</p>
※末尾に1行追記します
ログイン処理とログアウト処理
ログイン処理がないため、先ほど作ったログインのView画面でloginボタンをクリックしても、Session#createというタイトルのページが表示されるだけで、何もおきません。
では、ここから、ログイン処理(とログアウト処理)を作り込んでいきましょう。
ログイン処理は、コントローラのcreateメソッド、ログアウト処理はコントローラのdestroyメソッドで実装します。
コントローラは、これらのメソッドがありますが、空になっています
(app/controllers/sessions_controller.rb)
class SessionsController < ApplicationController
def new
end
def create
end
def destroy
end
end
そのため、それぞれ実装していき、最終的には以下のようになります。
(app/controllers/sessions_controller.rb)
class SessionsController < ApplicationController
# ログイン画面
def new
end
# ログイン検証とセッションの生成
def create
user = User.find_by!(mail: params[:mail]) # 入力されたメールアドレスからユーザーを検索
if user.authenticate(params[:password]) # パスワードを検証
session[:user_mail] = user.mail # 検証OKなら、セッションにメールアドレスを保存
redirect_to users_path # 一覧画面へ遷移
else
render ‘new’ # パスワード検証NGなら、ログイン画面を再表示
end
end
# ログアウト(セッションの破棄)
def destroy
session[:user_mail] = nil # セッション情報を削除
redirect_to login_path # ログイン画面へ遷移
end
end
コメントに記載していますが、それぞれのメソッドの詳細を説明しましょう。
newメソッド
ログイン画面の表示だけですので、何もしません。
createメソッド
ログイン画面で「login」ボタンをクリックした場合のPOSTリクエストのアクションです。
まずは、入力されたメールアドレスから、ユーザー情報を検索して取得します。
その後、has_secure_passwordメソッドと同時に導入された、パスワード検証用のauthenticateメソッドへ入力されたパスワードを渡して、検証します。
authenticateメソッドは、ユーザー情報のpassword_digestを復号化して、引数のパスワードとの一致チェックを行い、その結果をtrue/falseで返します。
パスワードの検証結果がtrueなら、メールアドレスをセッションへ保存して、一覧画面へ遷移します。
セッションへの保存は、sessionというハッシュへ任意のキーで保存するだけです。(今回は、user_mailというキーでメールアドレスを保存しました)
検証結果がfalseだった場合は、ログイン画面を再表示しているだけです。(本来であれば、メッセージなどを表示すべきでしょう)
destroyメソッド
保存していたセッション情報をnilで上書きして空にすることで、セッションを無効にしています。
その後、ログイン画面へ遷移します。
ログイン状態の確認
ここまでに、ログインとログアウトまでできましたので、ほぼ完成ですが、今のままでは、直接URLでアクセスすることで、ログインせずに操作ができてしまいます。
そこで、ログイン状態を確認して、ログインしていなければログイン画面へ遷移させる処理を追加します。すべてのコントローラが対象になり得ますので、ログイン状態の確認処理は、「app/controllers/application_controller.rb」に実装します。
(app/controllers/application_controller.rb)
class ApplicationController < ActionController::Base
def require_login!
if session[:user_mail].nil?
redirect_to login_path
end
end
end
require_login!メソッドは、セッション情報が空(nil)の場合はログイン画面へ遷移させるメソッドです。
つまり、直接アクセスされても、まずこのメソッドを実行すれば、ログインしていなければログイン画面へ強制的に遷移されるわけです。
ただし、新規作成やログイン画面そのものでは、この確認は必要ありません。そのため、この確認が必要なアクセスを確認して、適切な場所で実行するようにしましょう。
まずは、「rails routes」でルーティング設定を確認し、ログイン状態が必要なシーンを確認します。
[bash]>rails routes
Prefix Verb URI Pattern Controller#Action
login GET /login(.:format) sessions#new
POST /login(.:format) sessions#create
logout GET /logout(.:format) sessions#destroy
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
[/bash]
ログイン中でなければ行わせたくない処理は、ユーザーの一覧、ユーザー情報の詳細表示、ユーザー情報の変更と削除です。
つまり、Userコントローラのindex、edit、show、update、destroyアクションについては、ログイン状態の確認を行う必要があるでしょう。また、セッションコントローラのdestroyアクションも、ログイン状態を確認しておくべきです。
そのため、これらのアクションへアクセスされたときに、まずrequire_login!メソッドが呼ばれるように、それぞれのコントローラの先頭へ「before_action」を追記します。
(app/controllers/sessions_controller.rb)
class SessionsController < ApplicationController
before_action :require_login!, only: [:destroy]
# ログイン画面
:
(app/controllers/users_controller.rb)
class UsersController < ApplicationController
before_action :require_login!, only: [:index, :edit, :show, :update, :destroy]
before_action :set_user, only: [:show, :edit, :update, :destroy]
:
お疲れ様でした。
これにて、ログイン状態を保持することができる、最低限のログイン機能が完成しました。
まとめ
Railsのセッション機能について、説明しました。
今回は、実際にログイン機能を作り込んでいただきましたので、少しボリュームを感じたかもしれません。
しかし、実際には、以下の通りとてもシンプルなものです。
・HTTPはステートレスなため、ページ間で値を保持できない
・セッション(session)とは、ステートレスなHTTPで値を保持するための機能
・Railsのセッションは、任意のキーのハッシュsessionへ特定の値を保存することで実現される
・セッションの保存場所のデフォルトは、ブラウザのCookieになっている
・セッションの主な用途はログイン状態の保持だが、文字列情報であればどんな情報でも保存可能(サイズ制限はある)
「セッションを使うことで、ページ間で値を保持することができる」という点をしっかりと押さえていただければ、今回の記事は成功です。
なお、今回ご紹介したログイン機能は、極めて基本的なものですが、考え方のベースとして重要な内容を散りばめています。ぜひ、何度も復習して自分のものにしていただければと思います。