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上での「ベタ」です。
セッション管理の流れ
新規登録時
- ユーザ作成にあわせて、remember_token(セッショントークン)生成
- 暗号化の上dbに保管
ログイン時
- 新たにremember_token生成
- cookieにremember_tokenを保管
- 暗号化の上dbにも保管
- コントローラからもビューからもアクセスできる@current_userを準備
- ユーザアクセスの度、cookieの中身とdbの中身を擦り合わせて照合
- サインアウトでセッションを破棄する
※ 新規登録時は、連続してログインフローに乗る
セッションリソースの扱い
通常(Usersなど)のリソースと同様、RESTfulに管理する
HTTPリクエスト | URL | 名前付きルート | アクション | 用途 |
---|---|---|---|---|
GET | /signin | signin_path | new | 新しいセッション用 (サインイン) |
POST | /sessions | sessions_path | create | 新しいセッションを作成する |
DELETE | /signout | signout_path | destroy | セッションを削除する (サインアウト) |
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を利用する。
サインイン(ログイン)機能実装
- サインインフォーム準備
- フォーム値の受け取り(createメソッド)
- バリデーション
- サインイン実行(セッションを張る)
- sign_in(ほか関連)メソッドの実装
サインインフォームの作成
<% form_for(:session, url: sessions_path) %>
<% end %>
セッションはモデルが存在しないため、リソース名と対応するURLを指定する必要がある。
<%= 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メソッド)
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])
サインイン実行(セッションを張る)
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(ほか関連)メソッドの実装
- サインイン状態を永続化
- ユーザが明示的にサインアウトしたときのみ破棄
流れ
- 新たにremember_token生成
- cookieにremember_tokenを保管
- 暗号化の上dbにも保管
- コントローラからもビューからもアクセスできる@current_userを準備
- ユーザアクセスの度、cookieの中身とdbの中身を擦り合わせて照合
- サインアウトでセッションを破棄する
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?でログイン判定し、リンクを出し分けるサンプル
<% 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
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
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登録のタイミングで生成する。
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
これでサインイン時セッションを張り、サインアウトで破棄する一連のロジックが完成した。