LoginSignup
123
120

More than 5 years have passed since last update.

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

Posted at

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

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

123
120
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
123
120