chapter8では、セッションを利用してログイン・ログアウトを実装しましたが、chapter9ではcookieを利用して、永続的なログイン・ログアウトの実装をします。これはそこまでセキュリティーが求められないWebサービス向きです。
今回の実装のまとめ
1、Userモデルにremember_digestを作成
2、remember_digestにハッシュ化したトークンを保存(ハッシュは不可逆)
3、cookieにトークンと暗号化されたuser_idを保存
4、authenticated?(user)メソッドを使ってcookies[:remember_token]がremember_digestと一致することを確認
上で説明した設計やセキュリティ上の考慮事項を元に、次の方針で永続的セッションを作成する。
remember_digestカラムをUserモデルに追加
$ rails generate migration add_remember_digest_to_users remember_digest:string
rememberメソッド
以下は、要はランダムなトークンを作成し、それをハッシュ化して、データベースのremember_digestカラムに保存するということ。
class User < ApplicationRecord
attr_accessor :remember_token
・
・
・
# 渡された文字列のハッシュ値を返す
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
end
SecureRandom.urlsafe_base64とは?
# ランダムなトークンを返す
$ rails console
>> SecureRandom.urlsafe_base64
=> "brl_446-8bqHv87AQzUj_Q"
ハッシュ化について以下のサイトが分かりやすかったです。
https://it-trend.jp/encryption/article/64-0065
このサイトによると、
ハッシュ化とは、ハッシュ関数と呼ばれる特殊な計算方法によって、一見ランダムに見える別の値(ハッシュ値)にデータを変換する方法です。ハッシュ値は復号できないため、パスワードを保管する際などに活用されています。
同じデータから得られるハッシュ値は常に同じです。この特徴から、ハッシュ値を比較すれば元のデータが同一か否かを判断できます。
ログイン状態の保持
user.rememberメソッドが動作するようになったので、ユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存して、永続セッションを作成する準備ができました。
これを実際に行うにはcookiesメソッドを使います。このメソッドは、sessionのときと同様にハッシュとして扱えます。個別のcookiesは、1つのvalue(値)と、オプションのexpires(有効期限)からできています。有効期限は省略可能です。
ユーザーIDをcookiesに保存するには、sessionメソッドで使ったのと同じパターンを使います。
具体的には次のようになります。
cookies[:user_id] = user.id
しかしこのままではIDが生のテキストとしてcookiesに保存されてしまうので、アプリケーションのcookiesの形式が見え見えになってしまい、攻撃者がユーザーアカウントを奪い取ることを助けてしまう可能性があります。これを避けるために、署名付きcookieを使います。これは、cookieをブラウザに保存する前に安全に暗号化するためのものです。
cookies.signed[:user_id] = user.id
cookies.permanent.signed[:user_id] = user.id
cookiesを設定すると、以後のページのビューでこのようにcookiesからユーザーを取り出せるようになります。
User.find_by(id: cookies.signed[:user_id])
ログインでは、メールアドレスで検索をかけていましたが、今回は使えません。
したがって、暗号化のuser_idを使用することでセキュアな状態になります。
authenticated?(remember_token)メソッドの実装
bcryptを使ってcookies[:remember_token]がremember_digestと一致することを確認します。
has_secure_passwordでは、勝手にauthenticateメソッドが準備されましたが、cokkiesの場合は自ら用意する必要があります。今回はそれをauthenticated?(remember_token)とします。
中身は、モデルに保存されているremember_digestが仮想的値であるremember_tokenが一致するか確認してくれています。
# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
remember(user)の実装
rememberメソッドと紛らわしいですが、remember(user)メソッドを実装します。
中身は、rememberメソッドでUserのremember_digestにハッシュ化した値をupdate_attributeし、
cookiesのuser_idとremember_tokenにそれぞれ値を保存します。
後ほど記述するチェックボックスでチェックがついていれば、このメソッドが発火します。
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
forgetメソッドの実装
rememberとは反対にログアウトした時やチェックボックスにチェックが入っていない時に発火するforgetメソッドを実装します。中身はrememberと逆でremember_digestをnilにします。
# ユーザーのログイン情報を破棄する
def forget
update_attribute(:remember_digest, nil)
end
fotget(user)メソッドの実装
こちらもremember(user)と同様にforgetメソッドと紛らわしいですね。
こちらは、forgetメソッドでremember_digestをnilにすることに加えて、cookiesに保存されているuser_idとremember_tokenを削除します。
要は、
rememberとremember(user)
forgetとforget(user)
はそれぞれ反対のことをしているだけですね。
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
.
.
.
# 永続的セッションを破棄する
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
current_userの修正
cookiesが関わってきたことによって、current_userの修正が必要です、
要は、sessionの1回だけでなく、cookiesが加わり、ログインするための2回のチャンスが与えられるようになるわけです。
sessionにuser_idがなければ、cookiesからuser_idを探します。
もしあった場合は、user_idでユーザーを検索し、存在していて、かつauthenticated?メソッドがtrueであれば、ログインするという流れですね。
若干複雑に見えますが、やっていることはそこまで複雑ではありません。
sessionを使ってログインチャレンジ!
ダメだったら、
cookiesを使ってログインチャレンジ!
って感じですね。(雑)
module SessionsHelper
・
・
・
# 記憶トークン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
# ユーザーがログインしていればtrue、その他ならfalseを返す
def logged_in?
!current_user.nil?
end
end
チェックボックスの実装
後は、form_withの中にparams[:session][:remember_me]でデータが飛んでくるようにします。
params[:session][:remember_me]
チェックボックスがオンのときに'1'になり、オフのときに'0'になります。
paramsハッシュのこの値を調べれば、送信された値に基いてユーザーを記憶したり忘れたりできるようになります。
if params[:session][:remember_me] == '1'
remember(user)
else
forget(user)
end
次のような「三項演算子(ternary operator)」を使うと、このようなif-thenの分岐構造を1行で表すことができます。
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
以上でcookiesの実装が完了です。
個人的にはBCryptが絡んでくるところが理解しづらいところでした。
まとめると、
・remember_digestにハッシュ化した値を保存
・cookies[:user_id]とcookies[:remember_token]
後は、authenticated?(user)メソッドにより、上の一致を確認する。
他は以下のメソッドをそろった材料で実装していけば良いです。
rememberメソッド、remember(user)メソッド
forgetメソッド、forget(user)メソッド
current_userメソッド
chapter8に比べればレベルは上がりましたが、楽しかったです。