Posted at

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

More than 5 years have passed since last update.

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


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