こんな人におすすめ
- プログラミング初心者でポートフォリオの作り方が分からない
- Rails Tutorialをやってみたが理解することが難しい
前回:#8 ログイン/ログアウト, FactroyBot編
次回:#10 リメンバーミー機能編
今回の流れ
- 今回のゴールを把握する
- 永続化の仕組みを確認する
- 永続化に必要な属性を作る
- 永続化に必要なメソッドを作る
- Sessionsコントローラーを修正する
- current_userメソッドを修正する
- Userモデルのテストを作る
※ この記事は、ポートフォリオを作る理由をweb系自社開発企業に転職するためとします。
※ 2020年4月6日、記事を大幅に更新しました。
今回のゴールを把握する
ゴールは、ログインを永続化する機能を完成させることです。
永続化を、ユーザー自身に選択させるチェックボックスは、#10で作ります。
まずは、永続化の仕組みを確認します。
その際、永続化に必要なメソッドを確認します。
続いて、Userモデルにremember_digest属性を作ります。
続いて、永続化に必要なメソッドを作ります。
その際、複雑なコード記述について確認します。
続いて、ログイン時に永続化するようSessionsコントローラーを修正します。
続いて、クッキーを参照するようcurrent_userメソッドを修正します。
最後に、Userモデルのauthenticated?メソッドをテストします。
他の永続化に関するテストは、次回リメンバーミー機能を追加してからにします。
以上です。
永続化の仕組みを確認する
永続化は、以下の手順で行われます。
- ログイン時に、トークン、暗号化したトークン、暗号化したIDの3つを作る
- トークンと暗号化したIDをcookiesに、暗号化したトークンをDBに保存する
- 再びWebページに訪れた時、暗号化したIDからユーザーを特定する
- ユーザーを特定したら、トークンと暗号化したトークンを照合する
- 照合が成功したら、自動でログインする
- (永続化しない場合)、永続化を破棄する
以上から、使用するメソッドは以下の通りです。
- トークンを作る → User.new_tokenメソッド
- 暗号化したトークンを作る → User.digestメソッド
- 暗号化したIDを作る → cookies.signedメソッド
- トークンと暗号化したIDをcookiesに代入する → cookies.signedメソッド
- 暗号化したトークンをDBに代入する → remember(モデル)メソッド
- 作った3つを保存する → remember(ヘルパー)メソッド
- 暗号化したIDからユーザーを特定する → cookies.signedメソッド
- トークンを照合する → authenticated?メソッド
- 永続化を破棄する → forget(モデル)メソッド、forget(ヘルパー)メソッド
メソッドを使うには、暗号化したトークンをDBに保存するための属性が必要です。
その為、先にremember_digest属性を作ります。
その後、メソッドを作ります。
参考になりました↓
クッキー(cookie)とは?初心者でも分かるように図解
永続化に必要な属性を作る
Userモデルにremember_digest属性を加えます。
後述するメソッドを作る際に必要です。
$ rails generate migration add_remember_digest_to_users remember_digest:string
$ rails db:migrate
永続化に必要なメソッドを作る
永続化に必要なメソッドを作ります。
以下が、完成したメソッドです。
class User < ApplicationRecord
attr_accessor :remember_token
# 中略
# この部分はクラスメソッドです。先に解説します。
class << self
def new_token
SecureRandom.urlsafe_base64
end
def digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
end
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
def forget
update_attribute(:remember_digest, nil)
end
end
module SessionsHelper
# 中略
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
end
各メソッドを確認する前に、クラスメソッドについて確認します。
クラスメソッドについて知る
User.digestやUser.new_tokenはクラスメソッドです。
クラスメソッドは、このように定義します。
def User.digest(string)
end
def User.new_token
end
上記は、以下のように書き換えることができます。
class << self
def digest(string)
end
def new_token
end
end
前回までに定義してきたメソッドは、インスタンスメソッドと呼びます。
クラスメソッドとインスタンスメソッドの違いは、以下の通りです。
- クラスメソッド → クラスオブジェクトから呼び出すメソッド
- インスタンスメソッド → インスタンスオブジェクトから呼び出すメソッド
例をあげます。
# インスタンスオブジェクトを生成します
user = User.find(1)
# 以下のように呼び出せるのがインスタンスメソッドです
user.hoge
# 上記のように呼び出せず、以下のように呼び出すのがクラスメソッドです
User.hoge
では、なぜクラスメソッドを使う必要があるのでしょうか。
理由は、ユーザー情報が不要なメソッドを明示的に分けるためです。
トークンを作るだけ、暗号化するだけの処理には、ユーザ情報が不要です。
反対に、remember_digestは個々のユーザーが持つべき情報です。
その場合、インスタンスメソッドになります。
その他、クラスメソッドはクラスそのものの変更や参照にも使われます。
参考になりました↓
Rubyのクラスメソッドとインスタンスメソッドの例
【Ruby】クラスメソッドとインスタンスメソッドについてザクッと分かりやすく説明してみる
各メソッドを確認する
完成した各メソッドを確認します。全部で7つです。
メソッドの種類ごとに、改めてこちらに載せておきます。
(cookies.signedメソッドは標準で用意されているので、自ずとリストから外れます)
モデルメソッド
- トークンを作る → User.new_tokenメソッド
- 暗号化したトークンを作る → User.digestメソッド
- 暗号化したトークンをDBに代入する → remember(モデル)メソッド
- トークンを照合する → authenticated?メソッド
- 永続化を破棄する → forget(モデル)メソッド
ヘルパーメソッド
- 作った3つを保存する → remember(ヘルパー)メソッド
- 永続化を破棄する → forget(ヘルパー)メソッド
順に理解します。
User.new_token
トークンを作るメソッドです。
ランダムな文字列を生成する、urlsafe_base64メソッドを使っています。
class << self
def new_token
SecureRandom.urlsafe_base64
end
end
User.digest
暗号化したトークンを作るメソッドです。
BCryptから、ハッシュ値を返すメソッドを借りることで成立します。
class << self
def digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
end
参考になりました↓
ハッシュ値 (hash value)とは
remember(Userモデルメソッド)
暗号化したトークンをremember_digest属性に代入するメソッドです。
暗号前のトークンを一時的に扱うためには、仮属性remember_tokenを使います。
仮属性により、暗号前のトークンをユーザーと関連付けることができます。
同時に、暗号前のトークンをDBに保存せずに済みます。
仮属性を使うには、attr_accessorを使います。
attr_accessor :remember_token
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
参考になりました↓
Rubyのattr_accessorって何?[和訳]
Railsから入った人へ【attr_accessor】って?
永続cookiesでガチセッションするRailsチュートリアル9章
authenticated?
トークンを照合するメソッドです。
BCryptから、照合するメソッドを借りることで成立します。
このメソッドの、引数remember_tokenはローカル変数です。
このメソッドの、引数remember_digestはUserモデルの属性です。
1行目のif文は、複数のブラウザを同時に立ち上げた時のエラーを回避します。
片方のブラウザでログアウトしても、もう片方のブラウザにはクッキーが残ります。
この混線でcurrent_userが例外にならないよう、falseを代入します。
(詳しくは、後述の「current_userメソッドを修正する」をご覧ください。)
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
forget(Userモデルメソッド)
永続化を破棄するメソッドです。
remember_digestにnilを代入することで成立します。
def forget
update_attribute(:remember_digest, nil)
end
remember(Sessionsヘルパーメソッド)
トークン、暗号化トークン、暗号化IDを保存するメソッドです。
1行目にrememberヘルパーメソッド内で、rememberモデルメソッドを使っています。
つまりremember_tokenからremember_digestへの代入も行っています。
IDの暗号化には、cookies.signedを使います。
クッキーを長期間保存するには、cookies.permanentを使います。
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
forget(Sessionsヘルパーメソッド)
永続化を破棄するメソッドです。
モデルメソッドと異なるのは、クッキー側の破棄も行う点です。
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
Sessionsコントローラーを修正する
ログイン時に永続化するよう、Sessionsコントローラーを修正します。
先ほど、モデルメソッドでヘルパーを準備しました。
そのおかげで、ヘルパーのrememberメソッドを使うだけで永続化が成立します。
class SessionsController < ApplicationController
# 中略
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] = 'メールアドレスかパスワードが正しくありません'
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end
destroyアクションには、if文が追加されています。
これは、複数タブで同一Webページをみる際のエラーを回避しています。
このエラーは、一方のタブでログアウトしても、もう片方のタブに情報が残るために発生します。
log_outメソッドを確認すると、より具体的になります。
ログアウトするとcurrent_userにはnilが代入されます。
そこで再びログアウトしようとすると、forgetメソッドにnilを代入しようとします。
これがエラーの原因です。
def log_out
forget(current_user)
session.delete(:user_id)
@current_user = nil
end
current_userメソッドを修正する
クッキーを参照するようにcurrent_userメソッドを修正します。
current_userは以下のように、ユーザーを取得します。
- session[:user_id]が存在すれば、一時セッションからユーザーを取得する
- cookies[:user_id]が存在すれば、永続セッションからユーザーを取得する
module SessionsHelper
# 中略
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
# 中略
authenticated?メソッドでのエラー対応は、このif文への対応です。
複数ブラウザのうち一方をログアウトすると、remember_digestにnilを代入します。
しかし別ブラウザでクッキーが残ります。
そのためelsifを評価してしまいます。
ここでauthenticated?メソッドを確認すると、より具体的になります。
if文がなければ、BCrypt::Passwordクラスのnewメソッドにnilが代入されます。
このためのエラー回避です。
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
参考になりました↓
35歳だけどRailsチュートリアルやってみた。[第4版 9章 9.1 Remember me 機能 まとめ&解答例]
Userモデルのテストを作る
Userモデルのauthenticated?メソッドをテストします。
他の永続化に関するテストは、次回リメンバーミー機能を追加してからにします。
# 中略
describe "User model methods" do
describe "authenticated?" do
it "return false for a user with nil digest" do
expect(user.authenticated?('')).to be_falsey
end
end
end
今回は以上です。