Ruby on Rails チュートリアル:実例を使って Rails を学ぼう」の9章までのまとめです.

作成した機能

  • 静的ページ (ホーム, ヘルプ, アバウト, コンタクト)
  • ユーザー関係

    • ログイン機能
    • ユーザー作成/編集
    • ユーザー削除 (管理者のみ)
    • ユーザー一覧ページ
    • ユーザーページ (プロフィール表示)

ルーティング

config/routes.rb の中身は以下の通りです.
サインインページ, ユーザー作成ページ, サインアウトのみデフォルトの URL から変更しています.

SampleApp::Application.routes.draw do
  resources :users, except: :new
  resources :sessions, only: :create

  match '/signup',  to: 'users#new'
  match '/signin',  to: 'sessions#new'
  match '/signout', to: 'sessions#destroy', via: :delete

  root to: 'static_pages#home'

  match '/help',    to: 'static_pages#help'
  match '/about',   to: 'static_pages#about'
  match '/contact', to: 'static_pages#contact'

ページのURL一覧は以下のとおりです:

URL メソッド コントローラ アクション 役割
/signin GET SessionsController new サインインページ
/users GET SessionsController index ユーザー一覧ページ
/signup GET UsersController new ユーザー作成ページ
/users/:id GET UsersController show ユーザーページ
/users/:id/edit GET UsersController edit ユーザー編集ページ

静的ページは省略しました.
静的ページのコントローラは StaticPages で, ページごとにアクションを作成しています.

セッション開始/終了, ユーザーの作成/編集/削除のリクエストは以下の通りです:

URL メソッド コントローラ アクション 役割
/sessions POST SessionsController create サインイン
/signout DELETE SessionsController destroy サインアウト
/users POST UsersController create ユーザー作成
/users/:id PUT UsersController update ユーザー編集
/users/:id DELETE UsersController destroy ユーザー削除

rake routes の結果は以下:

      users GET    /users(.:format)          users#index
            POST   /users(.:format)          users#create
  edit_user GET    /users/:id/edit(.:format) users#edit
       user GET    /users/:id(.:format)      users#show
            PUT    /users/:id(.:format)      users#update
            DELETE /users/:id(.:format)      users#destroy
   sessions POST   /sessions(.:format)       sessions#create
     signup        /signup(.:format)         users#new
     signin        /signin(.:format)         sessions#new
    signout DELETE /signout(.:format)        sessions#destroy
       root        /                         static_pages#home
       help        /help(.:format)           static_pages#help
      about        /about(.:format)          static_pages#about
    contact        /contact(.:format)        static_pages#contact

コントローラ

コントローラ アクション 役割
StaticPagesController home ホームページ
help ヘルプページ
about アバウトページ
contact コンタクトページ
UsersController index ユーザー一覧ページ
show ユーザーページ
new ユーザー作成ページ
create ユーザー作成
edit ユーザー編集ページ
update ユーザー編集
destroy ユーザー削除
SessionsController new サインインページ
create サインイン
destroy サインアウト

UsersController はすべての REST アクションをもっています:

class UsersController < ApplicationController

  (省略)

  def index
    @users = User.paginate(page: params[:page])
  end

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(params[:user])
    if @user.save
      sign_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  def edit
  end

  def update
    if @user.update_attributes(params[:user])
      sign_in @user
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end

  def destroy
    @user = User.find(params[:id])
    if @user.admin?
      redirect_to(root_path)
    else
      @user.destroy
      flash[:success] = "User destroyed."
      redirect_to users_url
    end
  end

  (省略)

end

ここで signed_inSessionsHelper で定義しています(後述).

各アクションには現在のサインイン状態, 管理者かどうかなどによってアクセス制限をかけます. 具体的には,

  • 「ユーザーページ」「ユーザー作成ページ」「ユーザー作成」以外はサインインしないとアクセス不可とする.
  • 「ユーザー編集ページ」「ユーザー編集」はそのユーザー本人以外にはアクセス不可とする.
  • 「ユーザー削除」は管理者ユーザーのみ可能とする.
  • 「ユーザー作成ページ」「ユーザー作成」はサインイン中はアクセス不可とする.
  • 管理者ユーザーに対する「ユーザー削除」は禁止する.

とします.
これを実現するためには UsersController を以下のようにします:

class UsersController < ApplicationController
  before_filter :signed_in_user, only: [:index, :edit, :update, :destroy]
  before_filter :correct_user, only: [:edit, :update]
  before_filter :admin_user, only: :destroy
  before_filter :signed_in_user_redirect_to_root, only: [:new, :create]

  (省略 ※ REST アクション)

  private

  def signed_in_user
    unless signed_in?
      store_location
      redirect_to signin_url, notice: "Please sign in."
    end
  end

  def signed_in_user_redirect_to_root
    redirect_to(root_path) if signed_in?
  end

  def correct_user
    @user = User.find(params[:id])
    redirect_to(root_path) unless current_user?(@user)
  end

  def admin_user
    redirect_to(root_path) unless current_user.admin?
  end
end

ここで signed_in?, store_location, current_user?, current_userSessionsHelper で定義しています.

SessionsHelperApplicationController でインクルードしています.

module SessionsHelper

  def sign_in(user)
    cookies.permanent[:remember_token] = user.remember_token
    self.current_user = user
  end

  def signed_in?
    !current_user.nil?
  end

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

  def current_user=(user)
    @current_user = user
  end

  def current_user
    @current_user ||= User.find_by_remember_token(cookies[:remember_token])
  end

  def current_user?(user)
    user == current_user
  end

  def redirect_back_or(default)
    redirect_to(session[:return_to] || default)
    session.delete(:return_to)
  end

  def store_location
    session[:return_to] = request.url
  end
end

各メソッドの役割はメソッド名から想像がつくと思います.

redirect_back_or, store_location はフレンドリーフォワーディングを実現するためのメソッドです. サインインしていないユーザーがサインインしないと見られないページにアクセスした際, store_location で訪れたページをセッションに記録しサインインページにリダイレクトします. サインインした際, redirect_back_or によりサインイン後に表示させるページを制御します.

ここでサインイン, サインアウト, 現在のユーザー取得の各処理においてクッキーに user インスタンスの remember_token という属性の値(記憶トークン)を制御しています. 具体的には

  • サインイン時, そのユーザーの記憶トークンをクッキーに保存(sign_in).
  • サインアウト時, クッキーから記憶トークンを削除(sign_out).
  • クッキーの記憶トークンからユーザーを取得(current_user).

これはサインイン状態を永続化 (ブラウザを閉じてもサインイン状態を保持する) するための処理です.

サインイン/サインアウトは SessionsController で実現します:

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by_email(params[:email].downcase)
    if user && user.authenticate(params[:password])
      sign_in user
      redirect_back_or user
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    sign_out
    redirect_to root_url
  end
end

上記のようにサインイン処理を create アクション, サインアウト処理を destroy アクションに割り当てます.
サインイン完了後のリダイレクト処理は SessionsHelper で定義した redirect_back_or を用いています.
ページ表示のアクションはサインインページ表示の new のみになります.
user インスタンスの authenticate メソッドは has_secure_password という Rails の認証用機能を User モデルに持たせることで利用できる認証用のメソッドです.

最後に ApplicationController のすべてのコードを示します:

class ApplicationController < ActionController::Base
  protect_from_forgery
  include SessionsHelper

  def handle_unverified_request
    sign_out
    super
  end
end

2 行目の protect_from_forgery という記述により CSRF 攻撃に対する対策を行っています.
CSRF トークンの照合に失敗した場合, handle_unverified_request メソッドが呼ばれてセッションがクリアされます(reset_session メソッドが呼ばれる). 下記では handle_unverified_request をオーバーライドして確実にサインアウト処理を実行してからセッションがクリアされるようにしています.

モデル

作成したモデルは User のみです.

テーブル 属性 説明
users id INTEGER PRIMARY KEY ユーザーID
name varchar(255) ユーザー名
email varchar(255) メールアドレス
created_at datetime 作成日時
updated_at datetime 更新日時
password_digest varchar(255) パスワード
remember_token varchar(255) 記憶トークン
admin boolean 管理者フラグ

記憶トークンはサインイン状態を永続化するためのトークンです. ユーザーごとに異なり一意でかつ安全な (十分長くて十分ランダムな) 文字列です.

以下に User モデルのすべてのコードを示します.
上から 3 行目の記述により Rails の認証用機能 has_secure_password を有効にしています. この機能を有効にすると, データを保存する際, 自動的にパスワード (password) のハッシュ値を password_digest カラムに保存してくれます. attr_accessible の password, password_confirmation は仮想アトリビュートとなります.
記憶トークンの生成はモデル内で定義した create_remember_token メソッドで作成しています.

class User < ActiveRecord::Base
  attr_accessible :name, :email, :password, :password_confirmation
  has_secure_password

  before_save { |user| user.email = email.downcase }
  before_save :create_remember_token

  validates :name,
            {
                presence: true,
                length: { maximum: 50 }
            }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email,
            {
                presence: true,
                format: { with: VALID_EMAIL_REGEX },
                uniqueness: { case_sensitive: false }
            }
  validates :password,
            {
                length: {minimum: 6}
            }
  validates :password_confirmation,
            {
                presence: true
            }

  private

  def create_remember_token
    self.remember_token = SecureRandom.urlsafe_base64
  end
end

ここで,

before_save { |user| user.email = email.downcase }

は以下のようにしても同等です:

before_save { email.downcase! }