Rodhos Soft

備忘録を兼ねた技術的なメモです。Rofhos SoftではiOSアプリ開発を中心としてAndroid, Webサービス等の開発を承っております。まずはご相談下さい。

チュートリアルをやってみる。サインイン、サインアウト編

この記事は以下のチュートリアルをやってみた記録である。
第8章 サインイン、サインアウト | Rails チュートリアル

サインイン、サインアウトをするにはセッションの管理が必要となる。

  1. 忘却モデル ブラウザをとじることでセッション終了
  2. 継続モデル 「パスワードを保存する」チェックするとセッション継続
  3. 永続モデル ユーザが明示的にサインアウトするまでセッションが継続される

セッションにクッキーを利用する。

sessionコントローラを作る。

HTTPメソッドとRESTアクションの関係から、sessionコントローラのnewアクションがサインイン画面、createアクションがサインイン処理、destroyアクションがログアウトとする。

rails generate controller Sessions --no-test-framework

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
    def new
    end
    
    def create
    end
    
    def destroy
    end
end

画面
app/views/sessions/new.html.erb

<% provide(:title, "Sign in") %>
<h1>Sign in</h1>

<div class="row">
  <div class="span6 offset3">
    <%= 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 %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

form_forでは:sessionから情報を取り出して各フォームと関連づける。

ルーティング
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'
....

リソースも定義されている。

サインイン送信

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
...    
def create
  user = User.find_by(email: params[:session][:email].downcase)
  if user && user.authenticate(params[:session][:password])
    # ユーザーをサインインさせ、ユーザーページ (show) にリダイレクトする。
  else
    # エラーメッセージを表示し、サインインフォームを再描画する。
  end
end
...
end

つまり、paramsに入っている:sessionキーの値を取り出し、そこのemailに入っているものを小文字化したもので
ユーザを探し、もし、あってそれがパスワード認証を通過させるならOK。

失敗時のフラッシュメッセージを表示する。
class SessionsController < ApplicationController
....
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーをサインインさせ、ユーザーページ (show) にリダイレクトする。
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end
.....
end
成功時の処理

ユーザのサインイン処理を入れる

class SessionsController < ApplicationController
....
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      sign_in user
      redirect_to user
    else
      flash.now[:error] = 'Invalid email/password combination'
      render 'new'
    end
  end
.....
end

サインイン状態を永続化させて持っておき、サインアウトしたら解除される仕組みをつくる。
モジュールをつくり、コントローラとビューで使う。そのようなモジュールとしてSessionHelperがすでにできている。
これはビュー側にはインクルードされている。よって、コントローラ側にもインクルードする。

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

Railsセッションを用いて、記憶トークンを保持する。

session[:remember_token] = user.id

ユーザの取り出しは

User.find(session[:remember_token)

記憶トークンはユーザと結びつけておく必要があるため、usersテーブルにremember_tokenカラムを増やす。

マイグレーション生成

rails generate migration add_remember_token_to_users

db/migrate/20150915085034_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

マイグレート

 bundle exec rake db:migrate
 bundle exec rake db:test:prepare

記憶トークンとしてurlsafe_base64を使い、暗号化したものをdbに記録する。(トークンを盗まれたときのため)
記憶トークンサインインするたびに更新しておく。(セッションハイジャックのため)

before_createコールバックを利用して、ユーザが新規生成されるときから記憶トークンを設定するようにする。

app/models/user.rb

class User < ActiveRecord::Base
    before_save { self.email = email.downcase }
    before_create :create_remember_token
    
....    
    
    def User.new_remember_token
        SecureRandom.urlsafe_base64
    end
    
    def User.encrypt(token)
        Digest::SHA1.hexdigest(token.to_s)
    end
    
    private
    
    def create_remember_token
        self.remember_token = User.encrypt(User.new_remember_token)
    end
    
end
サインイン

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

  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

end

permanentで、トークンに期限をつけている。

= は空なら実行されfind_byが行われる。

サインインした際の変化をヘッダーにつける。
signed_in?でドロップアウトメニューを出している。
app/views/layouts/_header.html.erb

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="navbar-inner">
    <div class="container">
      <%= link_to "sample app", root_path, id: "logo" %>
      <nav>
        <ul class="nav pull-right">
          <li><%= link_to "Home", root_path %></li>
          <li><%= link_to "Help", help_path %></li>
          <% if signed_in? %>
            <li><%= link_to "Users", '#' %></li>
            <li id="fat-menu" class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                Account <b class="caret"></b>
              </a>
              <ul class="dropdown-menu">
                <li><%= link_to "Profile", current_user %></li>
                <li><%= link_to "Settings", '#' %></li>
                <li class="divider"></li>
                <li>
                  <%= link_to "Sign out", signout_path, method: "delete" %>
                </li>
              </ul>
            </li>
          <% else %>
            <li><%= link_to "Sign in", signin_path %></li>
          <% end %>
        </ul>
      </nav>
    </div>
  </div>
</header>

ドロップアウトメニューはBootstrapのJavaScriptを使うので
app/assets/javascripts/application.js

//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require turbolinks
//= require_tree .
ユーザ登録と同時にサインイン

現在は登録したあと自分でログインしないといけないので。

app/controllers/users_controller.rb

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

のように、ユーザが作られたときにそのままsign_inしてしまうようにする。

サインアウト

ルートへリダイレクト、サインアウトの処理の中身はヘルパーに書く。
app/controllers/sessions_controller.rb

  def destroy
    sign_out
    redirect_to root_url
  end

app/helpers/sessions_helper.rb

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

カレントユーザをクリアして、クッキーを消去。

ローカルサーバで確認できた。

rails server