Rails 総復習1ヶ月チャレンジ 3日目(Railsチュートリアル編)

3日目です!今日は8章 基本的なログイン機構のLog Inメソッドからです!

3日目(Ruby on Rails チュートリアル 8章 基本的なログイン機構 Log_Inメソッド〜)

ログイン機能続き

昨日からの続きからやっていきます。まずは実装の流れのおさらいです。

1: ブラウザを閉じるとログインを破棄する(Railsのsessionメソッドを用いる)
→ログインフォーム、Sessionsコントローラーまで実装済

2: ユーザーのログイン情報を自動で保存する(Railsのcookiesメソッドを用いる)

3: ユーザーがチェックボックスをオンにした場合のみログインを保存する(Remember me)

では1の続き、Log_in メソッドからやっていきます。

  • Log_inメソッド実装
    ログイン手法を様々な場所で使い回せるようにSessionsヘルパーにlog_inメソッドを定義します。
#app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end

注目はこの部分

session[:user_id] = user.id

Railsで事前定義済みのsessionメソッドを使いユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成されます。この後のページで、session[:user_id]を使ってユーザーIDを元通りに取り出すことができます。なおsessionsメソッドで作成された一時cookiesはブラウザで閉じた瞬間に有効期限が終了します。

Sessionsコントローラーのcreateアクションにlog_inメソッドを追加

#app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

補足: ぼっち演算子
sessions_controllerのcreateアクションの&&部分は、Rubyの”ぼっち演算子"と呼ばれる&.の表記でよりシンプルに出来ます。ぼっち演算子を使ってオブジェクトを呼び出すとオブジェクトがnilでない場合はその結果を、nilの場合はnilをそのまま返します。

#app/controllers/sessions_controller.rb

 if user && user.authenticate(params[:session][:password])
if user&.authenticate(params[:session][:password])
  • current_userメソッドを定義
    ユーザーIDを一時セッションの中に安全に置けるようになり、今度はそのユーザーIDを別のページで取り出すことにする為、current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにします。
#app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # 現在ログイン中のユーザーを返す(いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
end

注目はこの部分

 @current_user ||= User.find_by(id: session[:user_id])
    end

まずUser.find(session[:user_id]) ではなくUser.find_by(id: session[:user_id])とfind_byを用いているのはsession[:user_id]はnilの可能性がある為,nilだった場合に例外ではなくnilをそのまま返すようにしています。

ちなみに上記の一行は元々はこの形

if @current_user.nil?
  @current_user = User.find_by(id: session[:user_id])
else
  @current_user
en

これが三項演算子を用いて下記の形となります。

@current_user = @current_user || User.find_by(id: session[:user_id])

さらにRubyで「||=」(or equals)という代入演算子は下記ように短縮出来ます。

@current_user ||= User.find_by(id: session[:user_id])
    end
  • logged_inメソッドの定義
    ユーザーがログインしているときとそうでないときでレイアウトを変更(具体的にはログインしている時とログインしていない時のヘッダーを変えるなど)したいのでまずはlogged_inメソッドを定義します。
#app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # 現在ログイン中のユーザーを返す(いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end
end

ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_userがnilではないという状態を指します。これをチェックするには否定演算子、! を使っています。

ログイン中のユーザー用のレイアウトのリンクを変更します。

#app/views/layouts/_header.html.erb

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", '#' %></li>
          <li 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 "Log out", logout_path, method: :delete %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>
  • ユーザー登録の際にログインする ユーザー登録が完了すると自動でログインが出来るよう、Usersコントローラーにlog_inメソッドを追加します。
class UsersController < ApplicationController

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

  def new
    @user = User.new
  end

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

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end
  • ログアウト機能
    ログアウト処理ではlog_inメソッドの実行結果を取り消します。つまりセッションからユーザーIDを削除します。
# app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

注目は下記です。deleteメソッドを実行し、セッションからユーザーIDを削除しています。

session.delete(:user_id)

Sessonsコントローラーにlog_outメソッドを追加します。

#app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out
    redirect_to root_url
  end
end

これでsessionメソッドを使ったログイン機能が完成しました。

ユーザーのログイン情報を自動でユーザーの任意で保存する(permanent cookies)

sessionを用いたログインではユーザーが一度ブラウザを閉じてしまうとcookiesは有効期限が終了してしまいます。そこで、永続cookie(permanent cookies)を使ってブラウザを再起動した後でもすぐにログインできる機能(remember me)を実装します。(ブログの冒頭で示したログイン機能の2.3部分)
なお2.3はさらに長い為、手順をさらに細分化します。

①記憶トークン用のカラムを用意
②ランダムな文字列を生成し、ハッシュ化する
③ハッシュ化した値を、DBのカラムに保存する ④ブラウザのcookiesに、暗号化したIDとTokenを保存する
⑤cookiesのIDを使って、ユーザーをDBから検索する。cookiesのTokenを認証し、同一ならセッションを復元

かなり長いですが頑張ります・・・

①記憶トークン用のカラムを用意
remember_tokenをハッシュ化したものを入れる為のremember_digestカラムをUserモデルに用意します。

$ bundle exec rails generate migration add_remember_digest_to_users remember_digest:string
$ bundle exec rails db:migrate

②ランダムな文字列を生成し、ハッシュ化する,③ハッシュ化した値を、DBのカラムに保存する
続いてランダムな文字列を生成します。今回はRuby標準ライブラリのSecureRandomモジュールにあるurlsafe_base64メソッドを使います。またランダムな文字列をハッシュ化する為のUser.digestメソッドを合わせて実装します。

#app/models/user.rb

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

仮想属性のremember_tokenをUserモデルに追加します。また記憶トークンをユーザーと関連付け、トークンに対応する記憶ダイジェストをデータベースに保存するrememberメソッド作成する。

#app/models/user.rb

class User < ApplicationRecord
  attr_accessor :remember_token
  

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end

注目は下記

 def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
 end

ここではUser.new_tokenで生成されたランダム文字列を仮想属性remember_tokenに代入(selfをつけているのはRubyによってローカル変数を作成されるのを防ぐため、selfを与えると期待通りの属性が設定されます)。そしてremember_digestカラムにハッシュ化されたremember_tokenを入れています。なおupdate_attributeは条件に一致するモデルオブジェクトを更新するメソッドでバリデーションを無視するようになっています。なぜバリデーションを無視するものを使っているかというと、今回設定したtokenは全てコンピュータ側で設定し、人間が介在するところがなく失敗する可能性が極めて低い為、無理にバリデーションを走らせる必要がない為です。

④ブラウザのcookiesに、暗号化したIDとTokenを保存する
ハッシュ化したTokenをDBに保存までできたので次は実際にログイン状態の保持をしていきます。その為引数付きのrememberメソッドを新たにSessonsヘルパーを作成します。

#app/helpers/sessions_helper.rb

module SessionsHelper
:
  # ユーザーのセッションを永続的にする
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end
:

permanentはcookiesの有効期限を永続的(最長20年)付与します。signedは[:user_id]を暗号化しています。

⑤cookiesのIDを使って、ユーザーをDBから検索する。cookiesのTokenを認証し、同一ならセッションを復元

まずauthenticate?メソッド作成します。

#app/models/user.rb

class User < ApplicationRecord
 :
  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end

次にセッションの復元をします。

#app/helpers/sessions_helper.rb
:
 # 記憶トークンcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user &.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
:

これでセッションの復元までは完了しました。

  • forgetメソッドを実装 しかしこのままだと一度ログインしたら永続セッションが続いてしまうので終了できるメソッドを実装します。
#app/models/user.rb
:
  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
:

また引数付きのforgetメソッドをSessions_helperに定義します。そしてそれをlog_outメソッドから呼び出します。

#app/helpers/sessions_helper.rb

 .
  .
  # 永続的セッションを破棄する
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # 現在のユーザーをログアウトする
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end
:

Sessionsコントローラーのログアウトを少し変更します。

class SessionsController < ApplicationController
  .
  .
  .
  def destroy
    #ログイン中の場合のみログアウトする
    log_out if logged_in?
    redirect_to root_url
  end
end
  • remember meチェックボックス
    ユーザーが任意に永続化cookiesにするか選択できるようにします。

ログインフォームに追加

#app/views/sessions/new.html.erb

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

<div class="row">
  <div class="col-md-6 col-md-offset-3">
      :
      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      :
  </div>
</div>

ログインフォームから送信されたparamsハッシュには既にチェックボックスの値が含まれています。チェックボックスがオンの時に'1'になり、オフのときに'0'になります。 その値を使いコントローラーを変更します。

#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])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
       :
    end
  end
 :
end

これで実装完了です!次回は10章からやっていきます!