発展的なログイン機構 (Remember me 機能) | rails チュートリアル 13

第9章 発展的なログイン機構の勉強メモ。

ユーザーのログイン状態をブラウザを閉じた後でも有効にする [remember me] 機能を実装する。

ユーザーモデルに追加を行う。

$ bundle exec rails generate migration add_remember_digest_to_users remember_digest:string

すると、このようなファイルができる。

class AddRememberDigestToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :remember_digest, :string
  end
end

その後、migrateを行う。

$ bundle exec rails db:migrate
== 20170716204522 AddRememberDigestToUsers: migrating =========================
-- add_column(:users, :remember_digest, :string)
   -> 0.0765s
== 20170716204522 AddRememberDigestToUsers: migrated (0.0766s) ================

トークン生成用メソッドを追加する

ここからは、user.rememberメソッドを作成することを目標にする。

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

この「SecureRandom.urlsafe_base64」を実行すると、このメソッドは、A–Z、a–z、0–9、"-"、"_"のいずれかの文字 (64種類) からなる長さ22のランダムな文字列を返します。

$ bundle exec rails c
Running via Spring preloader in process 49093
Loading development environment (Rails 5.1.2)
Cannot read termcap database;
using dumb terminal settings.
[1] pry(main)> SecureRandom.urlsafe_base64
=> "TMem2uE-SIGWsx3AcZ3_ZA"
[2] pry(main)> SecureRandom.urlsafe_base64
=> "aWRRXRj-1DK95efPivsk0Q"
[3] pry(main)> SecureRandom.urlsafe_base64
=> "g8qtlMoXuPOOmbrOjD18fA"
class User < ApplicationRecord
  attr_accessor :remember_token
.
.
.
  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end

その後、class << selfを使ってdigestとnew_tokenメソッドを定義する。

class User < ApplicationRecord
.
.
.
  # 渡された文字列のハッシュ値を返す
  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
.
.
.
end
class User < ApplicationRecord
.
.
.
  # 渡された文字列のハッシュ値を返す
  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
.
.
.
end

これで、user.rememberメソッドが動作するようになった。

つまり、ユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存して、永続セッションを作成する準備ができた。

ログイン状態の保持

cookiesメソッドを使用して、記憶トークンと同じ値をcookieに保存するためには、以下のようなコードで可能。

cookies[:remember_token] = { 
    value:   remember_token,
    expires: 20.years.from_now.utc
}

この20年で期限切れになるcookies設定はよく使われるようになり、今ではRailsにも特殊なpermanentという専用のメソッドが追加された。
これにより、コードは次のようにシンプルにすることが可能。

cookies.permanent[:remember_token] = remember_token

ユーザーIDをcookiesに保存

ユーザーIDをcookiesに保存するには、

cookies[:user_id] = user.id

これを安全に暗号化するために、

cookies.signed[:user_id] = user.id

こうして、次はユーザーIDと記憶トークンはペアで扱う必要があるので、cookieも永続化するため、signedとpermanentをメソッドチェーンで繋いで、、

cookies.permanent.signed[:user_id] = user.id

とする。
こうすることで以下のコードで、cookiesからユーザーを取り出せるようになります。

User.find_by(id: cookies.signed[:user_id]) # あとで使う。

続いてbcryptを使用し、cookies[:remember_token]がremember_digestと一致することを確認します。

BCrypt::Password.new(remember_digest).is_password?(remember_token)

このコードで一致確認が可能です。
これをuserモデルに追加すると、

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

これで、ログインしたユーザーを記憶する処理の準備が整いました。rememberヘルパーメソッドを追加して、log_inと連携させる。

class SessionsController < ApplicationController
.
.
.
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      flash[:success] = 'log in'
      remember user # 追記
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end
.
.
.
end
module SessionsHelper
.
.
.
  # ユーザーを永続的セッションに記憶する
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # 記憶トークン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 && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
.
.
.
end

ユーザー情報を忘れる

ユーザーがログアウトできるようにするために、ユーザーを記憶するためのメソッドと同様の方法で、ユーザーを忘れるためのメソッドを定義します。
その機能をforgetメソッドとし、をUserモデルに追加する

class User < ApplicationRecord
.
.
.
  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
end

これで、永続セッションを終了できるようになる準備が整った。

log_outメソッドに、この機能を追加する。

module SessionsHelper
.
.
.
  # 永続的セッションを破棄する
  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
end

しかし、このままでは、複数のブラウザ対応ができていないため、修正していく。

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
  def destroy
    log_out if logged_in?
    flash[:success] = 'log out'
    redirect_to root_url # ログアウトしたらサイトトップへリダイレクト
  end

機能を使うかどうかの確認をユーザーに委ねる

<% provide(:title, "Log in") %>
<div class="page_mainvisual">
  <div class="container">
    <h1>Log in</h1>

    <%= form_for(:session, url: login_path) do |f| %>

      <table>
        <tbody>
          <tr>
            <th><%= f.label :email %></th>
            <td><%= f.email_field :email, class: 'form-control' %></td>
          </tr>
          <tr>
            <th><%= f.label :password %></th>
            <td><%= f.password_field :password, class: 'form-control' %></td>
          </tr>
        </tbody>
      </table>

      <!-- チェックボックス -->
      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      <p><%= f.submit "Log in", class: "btn btn-primary" %></p>
    <% end %>

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

</div>

このチェックボックスの値は、

params[:session][:remember_me]
# オンのときに’1’になり、オフのときに’0’

これで取得可能であり、1のときに、機能を使うように設定する。
三項演算子を使うとシンプルに記述が可能

params[:session][:remember_me] == '1' ? remember(user) : forget(user)

これをログイン機能を制御している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
      flash[:success] = 'log in'
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      # 0も1もRubyの論理値ではtrueなので注意
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    flash[:success] = 'log out'
    redirect_to root_url # ログアウトしたらサイトトップへリダイレクト
  end

end

herokuにup

herokuをメンテナンスモードにする場合と、その解除する方法は、

$ heroku maintenance:on
$ heroku maintenance:off

で、できる。
なので、herokuにupする一連の流れは、こんな感じで、今後やっていける。

$ git add .
$ git commit -m "メッセージ"
$ git checkout master
$ git merge XX
$ heroku maintenance:on
$ git push heroku master
$ heroku run rails db:migrate
$ heroku maintenance:off

メモ

testをすっ飛ばしているため、時々バグる。。。いつか直したい。

そして、すこしずつ難しくなってきているため、そろそろ復習していきたい。

復習は、ほぼ静的なページの作成からやりたい。

その前に rubyの勉強かなぁ。

railsチュートリアルも、あと、
第10章ユーザーの更新・表示・削除
第11章アカウントの有効化
第12章パスワードの再設定
第13章ユーザーのマイクロポスト
第14章ユーザーをフォローする
のこり、5章なので、この後半戦に行く前にやりたい。

コメント

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

内容に問題なければ、下記の「コメントを送信する」ボタンを押してください。


  1. KATOON.NET
  2. TRASH
  3. 発展的なログイン機構 (Remember me 機能) | rails チュートリアル 13