RailsTutorial9章

参考資料前提

f:id:shiness:20210113201732p:plain f:id:shiness:20210113201802p:plain [f:id:shiness:20210113201902p:plain f:id:shiness:20210113201912p:plain f:id:shiness:20210113201920p:plain

ユーザIDの暗号化 IDとTokenもcookieの中で暗号化したい。→@user.authenticate!した後にログイン不要になる。 attr_accessor データベースには保存しないがインスタンス変数には保存したい。

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

さしあたっての実装計画としては、user.rememberメソッドを作成することにします。このメソッドは記憶トークンをユーザーと関連付け、トークンに対応 する記憶ダイジェストをデータベースに保存します。リスト 9.1のマイグレーションは実行済みなので、Userモデルには既にremember_digest属性が追加 されていますが、remember_token属性はまだ追加されていません。このためuser.remember_tokenメソッドを使ってトークンにアクセスできるようにし、 かつ、トークンをデータベースに保存せずに実装する必要があります。そこで、6.3で行ったパスワードの実装と同様の手法でこれを解決します。あのとき は仮想のpassword属性と、データベース上にあるセキュアなpassword_digest属性の2つを使いました。仮想のpassword属性はhas_secure_passwordメソ ッドで自動的に作成されましたが、今回はremember_tokenのコードを自分で書く必要があります。これを実装するため、4.4.5で触れたattr_accessorを使 って「仮想の」属性を作成します。

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

rememberメソッドの1行目の代入にご注目ください。selfというキーワードを使わないと、Rubyによってremember_tokenという名前のローカル変数が成 されてしまいます。この動作は、Rubyにおけるオブジェクト内部への要素代入の仕様によるものです。今欲しいのはローカル変数ではありません。

  def User.new_token
    SecureRandom.urlsafe_base64
  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
class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { email.downcase! }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }


  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

  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end
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
      remember 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
module SessionsHelper
  
  def log_in(user)
    session[:user_id] = user.id
  end

  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
  
  def logged_in?
    !current_user.nil?
  end

  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

目立たないバグ

今のcurrent_userの使い方では、ユーザーが1つのタブでログアウトし、もう1つのタブで再度ログアウトしようとするとエラーになってしまいます。これは、もう1つのタブで "Log out" リンク>をクリックすると、current_userがnilとなってしまうため、log_outメソッド内のforget(current_user)が失敗してしまうからです(リスト 9.12)13 。この問題を回避するためには、ユーザー がログイン中の場合にのみログアウトさせる必要があります。

ユーザーが複数のブラウザ(FirefoxChromeなど)でログインしていたときに起こります。例えば、Firefoxでログアウトし、Chromeではログアウトせずにブラウザを終了させ、再度Chrome で同じページを開くと、この問題が発生します14 。FirefoxChromeを使った具体例で考えてみましょう。ユーザーがFirefoxからログアウトすると、user.forgetメソッドによって>remember_digestがnilになります(リスト 9.11)。この時点では、Firefoxでまだアプリケーションが正常に動作しているはずです。このとき、リスト 9.12ではlog_outメソッドによってユーザ ーIDが削除されるため、ハイライトされている2つの条件はfalseになります。

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