【Rails】sessionの使い方と仕組みを徹底解説【初心者向け】

Ruby on Rails

Ruby on Railsの学習初期で現れるsessionをは初心者には特殊な変数に見えますよね。
sessionはログイン・ログアウトを実現するための必須スキルです。

本記事ではsessionの使い方と仕組みを初心者向けに解説します。
Railsアプリケーション開発でsessionを使うための知識を身に着けておきましょう。

スポンサーリンク

セッションとは

WebアプリケーションにおいてセッションとはWebサーバーとブラウザ間で継続的なやり取りをするための仕組みです。セッションを利用すればユーザーがWebサイト上で行うアクションや操作などの情報を、異なるページやリクエスト間でも維持が可能となります。

HTTPプロトコルはステートレス

WebアプリケーションにおいてブラウザとWebサーバはHTTPというプロトコルで通信します。

HTTPはステートレスなプロトコルです。ステートレスとはステート(状態)がレス(無い)ということです。

例えばWebアプリケーションにログイン機能を付ける事を考えます。

  1. ブラウザはIDとパスワードをリクエストで送信
  2. Webサーバは受け取ったIDとパスワードを検証してログイン処理を完了し、レスポンスを返却
  3. ブラウザは個人情報などユーザの特定が必要な情報返すようにリクエスト

HTTPにおいて2回目のリクエストは1回目のリクエストを忘れています。
1回目のリクエストでログインにいくら成功していても、それを忘れているので情報を返すことができません。

セッションはステートレスなHTTPプロトコルを補完するための仕組み

「1回目のリクエストでログインが成功した」ということを覚えておくためにセッションを使用します。

  1. ログイン処理に成功したらユーザ情報をセッションストアに格納
  2. 以降のリクエストではセッションストアを参照し、ログイン済みであることを確認

HTTPではリクエスト間では直接的にデータを共有できません。そこでリクエスト外にセッションというデータストアを用意することでリクエスト間でデータを共有します。

Railsのsessionの使い方

sessionとはRailsでセッション管理を行うためのメソッドです。
sessionはハッシュのようにキーと値のペアでデータを保存できます。

値の設定

sessionはハッシュと同様の形式で利用できます。

session[キー] = "値"

例えば以下のように設定します。

session[:user_id] = 1

値の参照

sessionの値はハッシュと同様の形式で参照できます。

session[キー]

例えば以下のように参照します。

session[:user_id]

sessionの破棄

sessionを明示的に削除するには以下のように nil を設定します。

session[キー] = nil

以下のように clear メソッドを使用しても同じです。

session[キー].clear

例えば以下の様なコードになります。

session[:user_id] = nil
もしくは
session[:user_id].clear

キーを指定せず全てのsessionを破棄したい場合は以下のようにreset_sessionメソッドを使用できます。

reset_session

ログイン処理の実装

ログイン処理のリクエスト・レスポンスの流れ

以下が今回作成するログイン処理の流れです。

  1. ブラウザはログイン画面をリクエスト
  2. Webサーバはログイン画面のHTMLをレスポンスする
  3. ブラウザはログインフォームに入力されたID・パスワードを送信
  4. Webサーバは受け取ったID・パスワードをもとにログイン処理を行ない、成功したらセッションストアにユーザ情報を保存した上でトップページにリダイレクトレスポンス
  5. ブラウザは受け取ったリダイレクトURL(今回はトップページ)をリクエスト
  6. Webサーバはセッションからログイン済みのユーザ情報を参照し、トップページのHTMLをレスポンス
  7. Webブラウザは受け取ったHTMLを表示

わかりやすさのために装飾を除いた最低限の実装を行ないます。

なお、このサンプル実装はGithubにアップロードしてありますのでコードの全体像が知りたい場合は参照してください。
rails-login-sample| Github

ログイン画面

まずはログイン画面を作ります。

ログイン画面のルーティングを追加します。

Rails.application.routes.draw do
  get 'login', to: 'sessions#new'
end

ルーティングでsessions#newとしたのでSessionsControllerにnewアクションを追加します。

class SessionsController < ApplicationController
  def new
  end
end

ビューを最低限作ります。

<%= form_with(url: login_path) do |f| %>
  <div>
    <%= f.label :email %>
    <%= f.email_field :email %>
  </div>

  <div>
    <%= f.label :password %>
    <%= f.password_field :password %>
  </div>

  <div>
    <%= f.submit 'ログイン' %>
  </div>
<% end %>

http://localhost:3000/login にアクセスするとログイン画面が表示されます。

ログインアクション

ログインボタンをクリックしたときの処理を追加します。

ルーティングを追加します。

Rails.application.routes.draw do
  get 'login', to: 'sessions#new'
  post 'login', to: 'sessions#create' # 追加
end

sessions#createとしたのでSessionsControllerにcreateアクションを追加します。

class SessionsController < ApplicationController
  ...
  def create
    email = params[:session][:email]
    password = params[:session][:password]
    if login(email, password)
      # ログインに成功したらトップページにリダイレクトする
      redirect_to root_url
    else
      # ログインに失敗したらログイン画面に戻す
      render :new
    end
  end

  private
  def login(email, password)
    @user = User.find_by(email: email)

    return false unless @user && @user.authenticate(password)

    session[:user_id] = @user.id
    true
  end
end

Userはモデルです。authenticateメソッドはbcrypt gemが提供するメソッドで、モデルにhas_secure_passwordを書くことで実現できます。

以下のコマンドでUserモデルを生成します。

$ rails g model User name:string email:string password_digest:string

カラム名をpasswordではなくpassword_digestとするのはbcrypt gemのhas_secure_passwordが求めるためです。

Userモデルは以下のように修正します。

class User < ApplicationRecord
  has_secure_password
end

bcrypt gemをインストールします。Gemfileに以下を記述します。
コメントアウトされた形で書いてある場合はコメントアウトを外します。

gem "bcrypt", "~> 3.1.7"

バージョン(“~> 3.1.7”)は rubygems.org などを参照し、最新版を使用します。
bcrypt

bundle installでインストールしておきます。

またマイグレーションファイルも作成されているのでrails db:migrateでマイグレーションを実行しておきます。

ユーザ登録機能はないのでrails consoleで手動でユーザを作成しておきます。

$ rails c

irb(main):001:0> User.create(name: '太郎', email: 'taro@example.com', password: 'taro-abcd1234')

emailはtaro@example.com、パスワードはtaro-abcd1234としました。

rails s で実行し、再度http://localhost:3000/loginにアクセスした上でemail・パスワードを入力し、ログインボタンをクリックします。

すると以下のエラー画面が表示されます。

リダイレクト先であるトップページはまだ作成していないのでroot_urlが無いというエラーが出るのは正常です。
少なくとも正しくトップページにリダイレクトしようとしていることが確認できたのでこの段階ではこれでオーケーです。

ログイン後のリダイレクト先のトップ画面

トップページを作成してエラーを解消します。

ルーティングに root パスを追加します。

Rails.application.routes.draw do
  root 'top#index' # 追加
  get 'login', to: 'sessions#new'
  post 'login', to: 'sessions#create'
end

アクションをtop#indexとしたのでTopControllerを作成しindexアクションを追加します。
ビューでユーザ名を表示するためにUserモデルを取得してときます。
ユーザモデルのidはログイン時にセッションに保存したものを使用します。

class TopController < ApplicationController
  def index
    # ログイン時にセッションに保存した user_id を使ってユーザモデルを取得する
    @user = User.find_by(id: session[:user_id])
  end
end

アクションに対応するビューを作成します。
ビューにはログインユーザの名前を表示します。

ようこそ <%= @user.name %> さん

rails s で実行し、再度http://localhost:3000/loginにアクセスした上でemail・パスワードを入力し、ログインボタンをクリックします。

今度はエラーは表示されず、ログインユーザ名が表示されます。

ログアウト処理

最後にログアウト処理を追加します。

ログアウトのためのルーティングを追加します。

Rails.application.routes.draw do
  root 'top#index'
  get 'login', to: 'sessions#new'
  post 'login', to: 'sessions#create'
  delete 'logout', to: 'sessions#destroy'
end

アクションをsessions#destroyとしたのでSessionsControllerにdestroyアクションを追加します。

class SessionsController < ApplicationController
  ...

  def destroy
    session[:user_id] = nil
    redirect_to login_url
  end

  ...
end

sessions[:user_id] = nil とすることでセッションに入れたユーザIDを破棄しています。
これによってログインしていない状態にできます。

ビューにログアウト用のリンクを設置します。

ようこそ <%= @user.name %> さん
<%= form_with(url: logout_path, method: :delete) do |f| %>
  <%= f.submit 'ログアウト' %>
<% end %>

ログアウトボタンをクリックするとログイン画面にリダイレクトされます。

補足:未ログイン時にログイン画面にリダイレクトする

ここまでがログイン処理の基本的な流れです。
しかし、現状未ログイン状態でトップページにアクセスするとエラーとなります。

トップページはログインを必須としたい場合、コントローラのアクションをbefore_actionをつかってガードする必要があります。

Applicationコントローラにbefore_actionのためのメソッドを追加します。

class ApplicationController < ActionController::Base
  def require_login
    redirect_to login_url unless session[:user_id]
  end
end

require_loginメソッドはもしsession[:user_id]がnil(つまり未ログイン)の場合はlogin_urlでログインページにリダイレクトします。

TopControllerのindexアクションをrequire_loginメソッドでガードします。

class TopController < ApplicationController
  before_action :require_login # 追加
  def index
    @user = User.find_by(id: session[:user_id])
  end
end

これで未ログイン状態でhttp://localhost:3000/にアクセスするとログインページ(http://localhost:3000/login)にリダイレクトします。

補足: ログインしているUserのインスタンスを取得する便利メソッド

セッションに保存しているのはユーザのIDだけですので、ログインしているユーザの情報を取得したい場合は毎回User.findもしくはUser.find_byを実行しなければなりません。

セッションに保存されたユーザのIDからログインしているユーザのモデルを取得するメソッドを用意しておくと便利です。

ビューやコントローラで取得できるようにSessionHelperにメソッドを追加します。

module SessionsHelper
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
end

コントローラでも使用できるようにApplicationControllerでincludeしておきます。

class ApplicationController < ActionController::Base
  include SessionsHelper

  ...
end

トップページで使用していた @user は current_user に置き換えられます。

ようこそ <%= current_user.name %> さん
<%= form_with(url: logout_path, method: :delete) do |f| %>
  <%= f.submit 'ログアウト' %>
<% end %>

TopControllerでの取得処理は削除できます。

class TopController < ApplicationController
  before_action :require_login
  def index
    # @userはcurrent_userに置き換えたので削除
    # @user = User.find_by(id: session[:user_id])
  end
end

ログインユーザのインスタンスを取得するメソッド名にcurrent_userと名付けるのは慣習です。

Railsにおいてログイン処理を行うメジャーなライブラリにdeviseとSorceryがあります。
この2つのライブラリでもログインユーザを取得するメソッドが用意されており、両方とも名前はcurrent_userとなっています。
自分で考えることなく慣習に従い、current_userというメソッド名にするとよいでしょう。

セッションとデータベースとの使い分け

データベースもリクエスト外にデータを保存していますので、データベースを使用してもリクエスト間でデータを共有することができます。
セッションとデータベースのどちらに保存するかは、そのデータをずっと残したいか(永続化したいか)で決めます。

セッションには有効期限という概念があります。
最終アクセス後、決められた時間が経過すると自動的に削除されます。
一定日数や時間経過後にアクセスするとログイン画面に戻っているようなWebアプリケーションを使った事があると思います。これはセッションの有効期限後に自動削除される仕組みによります。

一方データベースには自動的に削除される仕組みはありません。

ユーザの名前は消えてしまうと問題になりますが、ログインしたユーザIDはセキュリティの観点から一定期間後には消えてほしいデータです。
そのためユーザ名はデータベースに格納し、ログインしたユーザIDはセッションに格納します。

まとめ

初心者向けにRailsのセッションについて解説しました。

ログイン処理はWebアプリケーションであればほとんどのアプリに搭載する機能です。
ログイン処理の中心となる技術はセッションなので、早いうちに理解しておくのが大切です。

タイトルとURLをコピーしました