1. Qiita
  2. 投稿
  3. Rails

[Rails] セッション管理をベタで実装してみる

  • 90
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Ruby on Rails Tutorialのエッセンスを自分なりに整理11

[Rails][RSpec] Capybaraでフォーム入力をシミュレートしてテストする
http://qiita.com/kidachi_/items/b0e607c83e9da9380d7e
の続き。

Ruby on Rails Tutorial(chapter8)
http://railstutorial.jp/chapters/sign-in-sign-out?version=4.0#top

概要

普通はDeviseなどの便利なgemやライブラリを使うのだろうが、
内部動作を知るためにまとめる。

※ あくまでRails上での「ベタ」です。

セッション管理の流れ

新規登録時

  1. ユーザ作成にあわせて、remember_token(セッショントークン)生成
  2. 暗号化の上dbに保管

ログイン時

  1. 新たにremember_token生成
  2. cookieにremember_tokenを保管
  3. 暗号化の上dbにも保管
  4. コントローラからもビューからもアクセスできる@current_userを準備
  5. ユーザアクセスの度、cookieの中身とdbの中身を擦り合わせて照合
  6. サインアウトでセッションを破棄する

※ 新規登録時は、連続してログインフローに乗る

セッションリソースの扱い

通常(Usersなど)のリソースと同様、RESTfulに管理する

HTTPリクエスト URL 名前付きルート アクション 用途
GET /signin signin_path new 新しいセッション用 (サインイン)
POST /sessions sessions_path create 新しいセッションを作成する
DELETE /signout signout_path destroy セッションを削除する (サインアウト)
config/routes.rb
SampleApp::Application.routes.draw do
  resources :users
  resources :sessions, only: [:new, :create, :destroy]
  root  'static_pages#home'
  match '/signup',  to: 'users#new',            via: 'get'
  match '/signin',  to: 'sessions#new',         via: 'get'
  match '/signout', to: 'sessions#destroy',     via: 'delete'
  ~
end

ただし、ストレージはCookie

通常のリソース(Users)などがバックエンドにDBを持つのに対して、
Sessionsリソースはcookiesを利用する。

サインイン(ログイン)機能実装

  1. サインインフォーム準備
  2. フォーム値の受け取り(createメソッド)
    • バリデーション
  3. サインイン実行(セッションを張る)
    • sign_in(ほか関連)メソッドの実装

サインインフォームの作成

erb
<% form_for(:session, url: sessions_path) %>
<% end %>

セッションはモデルが存在しないため、リソース名と対応するURLを指定する必要がある。

erb
    <%= form_for(:session, url: sessions_path) do |f| %>

      <%= f.label :email %>
      <%= f.text_field :email %>

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

      <%= f.submit "Sign in", class: "btn btn-large btn-primary" %>

    <% end %>

フォーム値の受け取り(createメソッド)

app/controllers/sessions_controller.rb
def create
  user = User.find_by(email: params[:session][:email].downcase)
  if user && user.authenticate(params[:session][:password])
    # ユーザーをサインインさせ、ユーザーページ (show) にリダイレクトする。
  else
    # エラーメッセージを表示し、サインインフォームを再描画する。
  end
end

値のバリデーション

  • 該当emailのユーザが存在するかどうか
User.find_by(email: params[:session][:email].downcase)
  • パスワードが適切かどうか
user.authenticate(params[:session][:password])

サインイン実行(セッションを張る)

app/controllers/sessions_controller.rb
def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      sign_in user
      redirect_to user    # userを解析し、'/users/:id'にリダイレクト
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end

sign_in(ほか関連)メソッドの実装

  • サインイン状態を永続化
    • ユーザが明示的にサインアウトしたときのみ破棄

流れ

  1. 新たにremember_token生成
  2. cookieにremember_tokenを保管
  3. 暗号化の上dbにも保管
  4. コントローラからもビューからもアクセスできる@current_userを準備
  5. ユーザアクセスの度、cookieの中身とdbの中身を擦り合わせて照合
  6. サインアウトでセッションを破棄する
app/helpers/sessions_helper.rb
module SessionsHelper

  def sign_in(user)
    remember_token = User.new_remember_token
    cookies.permanent[:remember_token] = remember_token
    user.update_attribute(:remember_token, User.encrypt(remember_token))
    self.current_user = user
  end

  def current_user=(user)
    @current_user = user
  end

  # signed_in?を経由して、セッションが張られているかどうかを確認する
  # つまり、(ほとんどの)リクエストの度に呼ぶようにする。
  def current_user
    remember_token = User.encrypt(cookies[:remember_token])
    @current_user ||= User.find_by(remember_token: remember_token)
  end

  def signed_in?
    !current_user.nil?
  end

  def destroy
    self.current_user = nil
    cookies.delete(:remember_token)
  end

end

どこに定義するか

セッション周りのメソッドはコントローラ/ビューのどちらからも使用できるよう、
SessionsHelperに定義(モジュール化する)

例えば

ビューでsigned_in?でログイン判定し、リンクを出し分けるサンプル

app/views/layouts/_header.html.erb
  <% if signed_in? %>
    <li><%= link_to "Users", '#' %></li>
    <li><%= link_to "Profile", current_user %></li>
    <li><%= link_to "Settings", '#' %></li>
    <li><%= link_to "Sign out", signout_path, method: "delete" %></li>
  <% else %>
    <li><%= link_to "Sign in", signin_path %></li>
  <% end %>

コントローラにinclude

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

すべてのヘルパーはビューではデフォルトで使用できる。
コントローラ側で使うには、明示的にapplication_controllerでインクルードしてやる。

remember_token生成ロジックの実装

Userモデルにremember_token属性を追加

$ rails generate migration add_remember_token_to_users
db/migrate/[ts]_add_remember_token_to_users.rb
class AddRememberTokenToUsers < ActiveRecord::Migration
  def change
    add_column :users, :remember_token, :string
    add_index  :users, :remember_token
  end
end
$ rake db:migrate

token生成

  • 一意性が担保できる、長くてランダムな文字列を用いる

SecureRandomモジュールのurlsafe_base64メソッドを利用

  def User.new_remember_token
    SecureRandom.urlsafe_base64
  end
  • 万一dbが攻撃されて漏洩された場合に備えて暗号化
  def User.encrypt(token)
    Digest::SHA1.hexdigest(token.to_s)
  end

どこに定義するか

remember_tokenの利用シーンは以下ふたつ。

  • サインイン時
  • 新規登録時

※新規作成されたユーザは連続してサインインさせるため
サインイン時のみremember_token作成すれば良い様にも思うが、
分離された役割である以上片方に依存すべきではない。
すべてのユーザが必ず最初から有効なremember_tokneを持つ様にすべき。

よって、(新規登録時に生成出来るよう)Userモデルに定義する。
→before_create(コールバック)を用いて、User登録のタイミングで生成する。

app/models/user.rb
class User < ActiveRecord::Base
  ~
  # User生成のタイミングでコールバック
  before_create :create_remember_token
  ~

  # ログイン時はSessionHelperから呼び出すことになるのでpublic
  def User.new_remember_token
    SecureRandom.urlsafe_base64
  end

  # 同上
  def User.encrypt(token)
    Digest::SHA1.hexdigest(token.to_s)
  end

  private

    # User生成時、before_createコールバックから呼び出す。
    def create_remember_token
      self.remember_token = User.encrypt(User.new_remember_token)
    end

end

これでサインイン時セッションを張り、サインアウトで破棄する一連のロジックが完成した。