平々凡々エンジニア

平凡で難しい悩みを解決

rails チュートリアル 9章 複数のブラウザでのログインログアウト処理が難しかった

8章まではなんとなく理解でき覚えておけそうだったのだが9章から複雑になってきたので長考したところや忘れそうなことはブログでまとめようと思います。

 

複数のブラウザで開いてどちらもログイン状態の場合に片方がログアウトしたあともう片方のログインしている方でログアウトしないでブラウザを閉じたあとサイドブラウザを立ち上げるとエラーになってしまうバグ解説が難しかった

(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 && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end

前提知識

current_user解説

セッションにuser_idが入っているとログイン状態

クッキーは永続ログイン化のために使用する

 

 

ここではfirefoxchromeで例とする

firefoxがログアウト以下のメソッドを実行する

(app/models/user.rb)

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

このメソッドは永続化ログインに使用されるクッキーに登録されている記憶トークンとDBに登録されている記憶ダイジェストのうちの一つ、DBに登録されている記憶ダイジェストを消去するメソッドである

 

このあとログインしているchromeのブラウザを閉じる

するとセッションは消える(session[:user_id]がnilになる)

そしてchromeブラウザを開きサイドアプリケーションにアクセスする

この時cookiesは残り続けているため以下の行を実行してしまう(記事一番最初のメソッドの一部)

(app/helpers/sessions_helper.rb)

 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

 

そして以下が実行される(app/models/user.rb)

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

ここで問題になるのはremember_digest

firefoxでログアウトした時にremember_digestはnilになってしまったので例外処理を吐き出してしまう

 

問題解決策(app/models/user.rb)

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

黄色くなっている行を追加する

 

テスト(user_test.rb)

  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end

この違うブラウザで複数の使用した時のテストは非常に再現することが難しいので

原因になった部分を再現することでテスト完了とする

 

学習している最中に思った疑問(混乱部分)

Q ログアウトした時にクッキーに登録しているユーザーIDを消してなかったけ?

A

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

で消してるよ

 

Q 結局どこがどうなってるのかよくわからなくなったから要点とポイントだけまとめて

ログアウトするとDBから永続化ログインするための情報が消える

と同時にブラウザのクッキー情報も消える

ブラウザごとにクッキーもセッションも違うので片方がクッキーが消えたからと言ってもう片方が消える訳ではない

DBから永続化ログインするための情報が消えているのでクッキーのみでログインしようとするとエラーになる

エラーになる部分はDBから永続化するための情報と記憶トークン(クッキー)を比べ一致するかをチェックするところクッキーはあるけどDBから永続化ログインするための情報がないのでエラー出力してしまう

DBから永続化するための情報がないときは即座にreturn falseして認証失敗にしてその後の処理は何もしない

 

自分の言葉でまとめた方が理解が深まった気がする