RailsTutorial9章
参考資料前提
[
ユーザ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 。
この問題を回避するためには、ユーザー がログイン中の場合にのみログアウトさせる必要があります。
ユーザーが複数のブラウザ(FirefoxやChromeなど)でログインしていたときに起こります。例えば、Firefoxでログアウトし、Chromeではログアウトせずにブラウザを終了させ、再度Chrome で同じページを開くと、この問題が発生します14 。FirefoxとChromeを使った具体例で考えてみましょう。ユーザーが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