Railsチュートリアルをまとめる
Railsチュートリアル第9章から多くのメソッドが入り乱れて訳が分からなくなりがちだったので、それぞれのメソッドとその役割を一章ごとにまとめていこうと思う。
Railsチュートリアル第8章までのコードを全て実装済であり、usersテーブルにremember_digestカラムを追加してあることが前提。
また、テストのことまで書くと膨大になってしまうため、開発環境の解説に留める。
##9.1 Remember_me機能
# 渡された文字列のハッシュ値を返す
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
class User < ApplicationRecord
attr_accessor :remember_token
.
.
.
# 永続セッションのためにユーザーをデータベースに記憶する
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
end
attr_accessorは、データベースに保存されないカラム(モデルに持たせるデータ)を作る、という感じに理解している。正確じゃないけど。
rememberメソッドでは、コメントに『永続セッションのためにユーザーをデータベースに記憶する』とあるように、そのような処理が書かれている。
attr_accessorで定義したremember_tokenにランダムなトークンを代入し、二行目でそれを暗号化して自身のrememer_digestカラムを上書きする、という流れ。
これで特定のuserに対してrememberメソッドを用いればremember_digestカラムに値が保存されるようになる。
最終的に、このremember_digestとremember_tokenが一致するかどうかを検証して永続ログインができるようにしていく。
ここから似た名前が連続して出てくるため紛らわしくなっていく。
# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
remember_digestとremember_tokenが同一かどうか調べるためのメソッド。
しかしこのメソッドを実際に使うのはもう少し後なので、解説は後回しにする。
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
createアクション内に永続ログイン用の処理を書き込む。
しかしここでもこの処理はまだ実装していないので、エラーになる。
上のuser.rbで定義したrememberメソッドとは名前が同じだけでここでの処理とは関係ない。上のメソッドには引数が必要ないことを確認。
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
このメソッドで永続ログインのためにcookieを設定する。
cookieはブラウザに保存されるテキスト情報のこと。
ここでuser.rbで定義したrememberメソッドが使用されている。このメソッドで行っているのはremember_tokenをremember_digestに保存すること。
つまり、ここでは引数で渡されたuserのremember_digestを更新している。
その下でcookiesメソッドでcookieを設定する。
cookies.permanent.signed[:user_id] = user.id
渡されたuserのidをsignedで保存される前に安全のために暗号化し、permanentで有効期限を20年に設定する。
cookies.permanent[:remember_token] = user.remember_token
これも上とほぼ同じ。ただ違うのが、こちらではsignedを使っていないこと。userの持つremember_tokenはランダムなトークンを設定するUser.new_tokenメソッドで生成されており、それをさらに暗号化する必要がないため、だと思う(違っていたらどなたかご指摘お願いします)。
# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
先ほど解説を後回しにしたauthenticatedメソッド。このメソッドは、この下のcurrent_userメソッド内で使用する。
引数がremember_tokenとなっているが、これはattr_accessorで定義したremember_tokenとは無関係のただの引数であることに注意。ここにはログイン時にremember(user)メソッドで設定したcookieに保存されたremember_tokenが代入される。
つまり、remember_tokenを使って設定されたcookieとremember_digestカラムの値が一致するかを検証しており、一致すればtrueを返す。
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
if user && user.authenticated?の部分で検証を行っている。
cookie.signed[:user_id]で検索したuserが存在し、かつ、そのuserのremember_digestとcookie[:remember_token]が一致すれば、という条件式。
def forget
update_attribute(:remember_digest, nil)
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
ログアウトの処理。ログアウトはだいぶシンプル。
forgetメソッドはremember_digestをnilに更新しているだけ。
def forget(user)はforgetメソッドでremember_digestをnilにし、二つのcookieを削除しているだけ。
最後にlog_outメソッド(実際にコントローラーに埋め込むメソッド)。
forgetメソッドの引数にcurrent_userを渡してそれに関わる上記三つのデータを削除し、sessionの値とcurrent_userをnilにしてログイン状態を抜ける。
続いて、このログアウトによって想定される二つのバグを消していく。
想定されるバグとは、二つ以上のタブで同時にアプリにアクセスし、片方でログアウトして片方でログインし続けると、ログアウトが正常に動かなくなってしまうこと。
もう一つは、別々のブラウザでアクセスし、片方だけでログアウトした時、ログインを続けていたブラウザを終了させて再度アクセスしたときにエラーが発生すること。
def destroy
log_out if logged_in?
redirect_to root_url
end
一つ目の問題は、ログイン中でないとログアウトができないと設定してしまえば解決。
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
二つ目の問題では、authenticated?メソッドをいじることで解決する。
他のブラウザでログアウトしてremember_digestがnilになってしまっていた場合にはfalseを返す。
つまり、current_user内のif user && user.authenticated?(cookies[:remember_token])の条件式がfalseになるため、またこの時点でsession[user_id]も削除されているため、ログインしていない状態に戻される。
##9.2 [Remember me] チェックボックス
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
params[:session][:remember_me]で1か0を取得できるチェックボックスをビューに設置。
1(チェックがついたとき)に永続ログインを行うようにする。
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
#これを追加
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
アクションの中にこれを組み込めば実装終了。
1(チェックがついたとき)だった場合にはremember(user)を、0(チェックがなかったとき)にはforget(user)を使う。
一応補足しておくと、def forget(user)はremember_digestとcookieを削除するメソッドなので、sessionには影響しない。
これでcookieを使った永続ログインかsessionのみの普通のログインかを選択できるようになっているはず。