railsでログインシステム

railsでログインシステムを試してみたのでメモ.
コード自体は「Railsによるアジャイルwebアプリケーション開発」の第14章タスク:ログインとほぼ同じ.
本格的なログイン機能が必要な場合プラグインを使う.Deviseというプラグインが人気らしい.
環境はRuby1.9.3p286とRails3.2.8

Gemfileに

gem 'bcrypt-ruby'

を追加してinstallしておく

$rails g controller home index

しておく,index.html.erbを編集する.
index.html.erb

<h1>ログインシステムのテスト</h1>
<ul>
  <li><a href="#">ログイン</a></li>
  <li><a href="#">ログアウト</a></li>
</ul>

webアプリには管理者と一般ユーザがいる.Userモデルが必要である.今回はこのUserモデルが管理人のことなので注意.一般ユーザモデルは実装しない.セッションを持続するためにSessionモデルも必要である.まずUserモデルを生成する.scaffoldを使えばOK

$rails g scaffold User name:string password_digest:string
$rake db:migrate

user.rbを編集する.

user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :password
  validates :name, presence: true, uniqueness: true
  has_secure_password
end

ユーザにshowメソッドは必要ないので新規登録(user/new)のあとshowにリダイレクトするのはやめる.user_conrtoller.rbを編集する.

createdメソッド後のredirect_to @userをredirect_to users_urlに変更,updateメソッドも同様,users_urlとはusers/indexのことindexはUser.allをUser.order(:name)とすることでアルファベット順に並び替える.
user_controller.rb

  # GET /users
  # GET /users.json
  def index
    @users = User.order(:name)

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @users }
    end
  end


  # POST /users
  # POST /users.json
  def create
    @user = User.new(params[:user])

    respond_to do |format|
      if @user.save
        format.html { redirect_to users_url, notice: 'User was successfully created.' }
        format.json { render json: @user, status: :created, location: @user }
      else
        format.html { render action: "new" }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end


  # PUT /users/1
  # PUT /users/1.json
  def update
    @user = User.find(params[:id])

    respond_to do |format|
      if @user.update_attributes(params[:user])
        format.html { redirect_to users_url, notice: 'User was successfully updated.' }
        format.json { head :no_content }
      else
        format.html { render action: "edit" }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

ビューを変更する.notice(お知らせ)を追加し,password_digestの表示を削除する.
users/index.html.erb

<h1>Listing users</h1>
  <% if notice %>
  <p id="notice"><%= notice %></p>
  <% end %>

<table>
  <tr>
    <th>Name</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @users.each do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= link_to 'Show', user %></td>
    <td><%= link_to 'Edit', edit_user_path(user) %></td>
    <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New User', new_user_path %>

パスワードのダイジェストのフォームをパスワードを確認のフォームに置き換える
users/_form.html.erb

<%= form_for(@user) do |f| %>
  <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <fieldset>
    <legend>ユーザ情報の入力</legend>

    <div>
      <%= f.label :name %>:
      <%= f.text_field :name, size: 40 %>
    </div>
    <div>
      <%= f.label :password, 'パスワード' %>:
      <%= f.password_field :password, size: 40 %>
    </div>
    <div>
      <%= f.label :password_confirmaion, '確認' %>
      <%= f.password_field :password_confirmaion, size: 40 %>
    </div>
    <div>
      <%= f.submit %>
    </div>

  </fieldset>

<% end %>

testを更新しておく.
users_controller_test.rb

require 'test_helper'

class UsersControllerTest < ActionController::TestCase
  setup do
    @input_attributes = {
      name: "sam",
      password: "private",
      password: "private"
    }
    @user = users(:one)
  end

  test "should get index" do
    get :index
    assert_response :success
    assert_not_nil assigns(:users)
  end

  test "should get new" do
    get :new
    assert_response :success
  end

  test "should create user" do
    assert_difference('User.count') do
      post :create, user: @input_attributes
    end

    assert_redirected_to users_path
  end

  test "should show user" do
    get :show, id: @user
    assert_response :success
  end

  test "should get edit" do
    get :edit, id: @user
    assert_response :success
  end

  test "should update user" do
    put :update, id: @user.to_param, user: @input_attributes
    assert_redirected_to users_path
  end

  test "should destroy user" do
    assert_difference('User.count', -1) do
      delete :destroy, id: @user
    end

    assert_redirected_to users_path
  end
end

管理者のページのため,セッション管理のためのコントローラを生成する.newはログイン画面のコントローラ,createはログインのコントローラ,viewは持たない.destroyはセッション終了のコントローラ,viewは持たない.

$rails g controller Sessions new create destroy
$rails g controller Admin index

createアクションはAdminがログイン済みであるかを確認する.もしログインがすんでいればsessionに記録しなおす.ログインがまだならnewアクションにリダイレクトし,ログインを促す.セッションにはsession[キー]でアクセスできる.destroyはセッションを終了する

sessions_controller.rb

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by_name(params[:name])
    if user and user.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to admin_url
    else
      redirect_to login_url, alert: "error"
    end
  end

  def destroy
    session[:user_id] = nil;
    redirect_to users_url
  end
end

newアクションはログインページを提供する.
sessions/new.html.erb

<% if flash[:alert] %>
  <p id="notice"><%= flash[:alert] %></p>
<% end %>

<%= form_tag do %>
  <fieldset>
    <legend>ログインしてください</legend>
    <div>
      <%= label_tag :name, '名前:' %>
      <%= text_field_tag :name, params[:name] %>
    </div>
    <div>
      <%= label_tag :password, 'パスワード' %>
      <%= password_field_tag :password, params[:password] %>
    </div>
    <div>
      <%= submit_tag 'ログイン' %>
    </div>
  </fieldset>
<% end %>

routes.rbを編集してレスポンスをrailsに知らせてやる必要がある
routes.rb

Login::Application.routes.draw do
  get 'admin' => 'admin#index'

  controller :sessions do
    get 'login' => :new
    post 'login' => :create
    delete 'logout' => :destroy
  end

  #…

アクセス制限機能を実装する.railsのフィルタ機能を使うと全てのページで共通の処理を実装することが出来る.通常は ApplecationContorllerにbefore_filterをかける.authorizefメソッド内部でsessionがあるかを調べ,無かったら/loginにリダイレクトする.
ApplectionController.rb

class ApplicationController < ActionController::Base
  protect_from_forgery
  before_filter :authorize

  private
    def authorize
      unless User.find_by_id(session[:user_id])
        redirect_to login_url, notice: "Please login"
      end
    end
end

このままではログインしなければwebサイトの機能を使うことができない.ログイン不要な部分はskip_before_filter機能を使い,スキップする.ログインページはskip_before_filterをしておかないと無限ループになり,ブラウザがアクセスを拒否することもあるようだ.
home_controller.rb

class HomeController < ApplicationController
  skip_before_filter :authorize

  def index
  end
end

sessions-controller.rb

class SessionsController < ApplicationController
  skip_before_filter :authorize

  def new
  end

  def create
    user = User.find_by_name(params[:name])
    if user and user.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to admin_url
    else
      redirect_to login_url, alert: "error"
    end
  end

  def destroy
    session[:user_id] = nil;
    redirect_to users_url
  end
end