#ユーザーを記憶するための手段(RailsTutorial9章)
##Cookiesメソッドを用いた永続ログインの方法
Cookiesメソッドによる、永続ログインの方法について、概要をバサッとまとめてみました。
-
永続ログインを可能にするために、ブラウザのcookieに記憶トークンと呼ばれる文字列を保存する。
-
この記憶トークンをセキュアな文字列にした記憶ダイジェストをデータベースに保存する。
-
この記憶トークンをデータベースのセキュアな文字列と比較し、照合する。
-
署名付き暗号化user_idもcookieに保存し、データベースのuser.idと比較する。
###記憶トークンの生成及び、ハッシュ値のデータベースへの保存
記憶トークンはSecureRandom の urlsafe_base64を使い生成
記憶ダイジェストは
def User.digest(string)
cost=ActiveModel::SecurePassword.min_cost? Bcrypt::Engine::MIN_COST :
Bcrypt::Engine.cost
Bcrypt::Password.create(string, cost: cost)
end
のような関数でUser.digestの引数に代入した値をBcryptでハッシュ化する。
ここでは、インスタンスメソッドで定義する必要はないので、クラスメソッドで定義している。
引数に代入する値は生成したランダムな64文字の文字列である記憶トークンを用いる。
def User.new_token
SecureRandom.urlsafe_base64
end
この二つの過程を合体させて生成した記憶トークンをハッシュ化した値をデータベースのremember_digestに保存する
この時、記憶トークンの属性も別途設定しておく
attr_accessor :remember_token
def remember
self.remember_token=User.new_token
Update_attribute(:remember_digest,User.digest(remember_token))
end
注意 self.remember_tokenとしないとremember_tokenのローカル変数を定義してしまうことになる。
###current_userにログイン中のユーザー情報を保持させる
1.sessionがまず存在するかどうかを確かめる
2.ない場合はcookiesに署名されたuser_id (つまりcookies.signed[:user_id])が存在するか確かめる
3.user=User.find_by(id:cookies.signed[:user_id])でUser モデルにcookiesのuser_idをもつユーザーが存在するか確かめる
注意 cookies.signed[:user_id]は暗号化されているが、自動的に暗号化が解除されている。
4.このuserが存在、つまりはtrueであり、かつ、そのuserのremember_digestとcookies[:remember_token]のハッシュ値が一致するとき、つまり
if user&& user.authenticated?(cookies[:remember_token]) ・・・・・注意
@current_user=user
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
としてcookiesの中のremember_tokenキーに含まれるトークンのハッシュ値が、userのremember_digestと一致するとき、
ログイン中にする。(@current_userにuserを代入する。)
1. ー4.をまとめてコードにすると
def current_user
if (user_id=session[:user_id])
@current_user||=User.find_by(id: user_id) #@current_userが存在する場合はそのまま、ない場合はデータベースからActive Recordのfind_byメソッドを用いて検索
elsif (user_id=cookies.signed[:user_id])
user=User.find_by(id: user_id)
if (user && user.authenticated?(cookies[remember_token]))#userが存在し、cookiesに保存されているトークンのハッシュ値とremember_digestが一致する場合
@current_user=user
end
end
end
注意:ここでuser_id=session[:user_id]とあるがこれは比較をしているのではなく、
user_idにsession[:user_id]を代入した際に存在すれば。の意味である。
また、ヘルパーでcookiesに署名付き暗号化user.idを保存し、64文字のトークンを保存することで、
cookiesからユーザー情報をトークンで取り出せるようにする。
def remember(user)
user.remember #データベース側にremember_digestを保存
cookies.permanent.signed[:user_id]=user.id #署名付き暗号化user_idをcookies側に保存
cookies.permanent[:remember_token]=user.remember_token #remember_tokenをcookies側に保存
end
これでcurrent_userにログイン中のユーザーの記憶する手段が整った。
###ログイン時に行われるログインの永続化の処理
def create
user=User.find_by(email: params[:session][:email].downcase)
if user && user.authenticated?(params[:session][:password])
log_in (user) #ヘルパーで定義 #current_userにuserを代入
remember(user)#ヘルパーで定義 #cookiesにremember_token及びuser_idを保存
redirect_to user_url(user)
else
flash.now[:danger]="Invalid email/password combination"
render 'new'
end
end
##Logoutするとき
ログアウトするときは、
- cookies.signed[:user_id]の削除
- cookies[:remember_token]の削除
- remember_digestをnilにする
- sessionの情報も破棄する
- current_userもnilにする
def forget
update_attribute(:remember_digest,nil)
end
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
def logout
forget(current_user)
session.delete(:user_id)
@current_user=nil
end
def destroy
log_out if logged_in?
redirect_to root_url
end
ここで
log_out if logged_in?
としている理由としては
ブラウザが2つ起動していて、片方のブラウザでログアウト処理をした場合に、current_userがnilになるのでforget(current_user)の引数にしているオブジェクトがnilになるのでエラーをはくためです。
もう一つエラーがあります。
同じようにブラウザが2つ起動していて、片方でログアウト処理をしたとき、remember_digestの値がnilになるので、もう片方のブラウザで一度ブラウザを閉じて、再びつけたときに、cookieは存在するので、下の式のcookies.signed[:user_id]が評価されるので次のif文まで評価されますが、authenticated?メソッドで使っているBcrypt.newは引数であるremember_digestがnilになってしまいエラーになってしまいますので、remember_digestがnilの場合は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])# BCryptで生成するdigestの値がnilとなりエラーが発生する。
log_in user
@current_user = user
end
end
end
def authenticated?(remember_digest)
return false if remember_digest.nil? #remember_digestがnilの場合はfalseを返して、条件式を終了させる。
BCrypt.new(remember_digest).is_password?(remember_token)
end
##まとめ
-
cookiesではログインするための情報を保存する
-
署名付き暗号化user_id
-
記憶トークン
-
ログインするときは、sessionから照合し、ない場合は、cookiesの情報を参照する。
-
データベースにはハッシュ化されたトークンを保存しておき、authenticated?ヘルパーにより、cookiesの記憶トークンと照合する。
-
ログアウトするときはcookiesやデータベースのremember_digestをnilにする。