##Remember me機能
ユーザーのログイン状態をブラウザを閉じた後でも有効にする[remember me]機能を実装する。
明示的にログアウトしなければログアウトできない。
ブランチを作成
$ git checkout -b advanced-login
###記憶トークンと暗号化
記憶トークンと暗号化、cookiesメソッドによる永続的cookiesの作成や、安全性の高い記憶ダイジェストに
トークン認証にこの記憶トークンを活用する。
sessionメソッドで保存した情報は自動的に安全が保たれている。
しかしcookiesメソッドに保存する情報は安全ではない。
盗み出す方法がいくらでもある。
永続的セッションを作成するには
1.記憶トークンにはランダムな文字列を生成して用いる。
2.ブラウザのcookiesにトークンを保存するときには、有効期限を設定する。
3.トークンはハッシュ値に変換してからデータベースに保存する。
4.ブラウザのcookiesに保存するユーザーIDは暗号化しておく。
5.永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。
ubuntu:~/environment/sample_app (advanced-login) $ rails generate migration add_remember_digest_to_users remember_digest:string
Running via Spring preloader in process 4908
invoke active_record
create db/migrate/20211008140754_add_remember_digest_to_users.rb
####記憶ダイジェスト用に生成したマイグレーション
db/migrate/[timestamp]_add_remember_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration[6.0]
def change
add_column :users, :password_digest, :string
# usersテーブルに
# genalateによって生成された絡むとそのデータ型
# カラムとデータ型を追加
end
end
ランダムな文字列生成
>> SecureRandom.urlsafe_base64
=> "ycRDo1SWia84lbXERcV6Bw"
>> SecureRandom.urlsafe_base64
=> "ncAMbSkaP32MVyNiyPYvZg"
>> SecureRandom.urlsafe_base64
=> "z-JdrOsqlPQaB2Ukp-PS0w"
このメソッドは、A–Z、a–z、0–9、"-"、"_"のいずれかの文字(64種類)からなる長さ22のランダムな文字列を返します。
####トークン生成用メソッドを追加する
app/models/user.rb
class User < ApplicationRecord
.
.
.
# ランダムなトークンを返す
def User.new_token
SecureRandom.urlsafe_base64
# ランダムな文字列を生成
end
end
####rememberメソッドをUserモデルに追加する
app/models/user.rb
class User < ApplicationRecord
.
.
.
# 永続セッションのためにユーザーをデータベースに記憶する
def remember
self.remember_token = User.new_token
# ランダムな文字列がトークンになる。
update_attribute(:remember_digest, User.digest(remember_token))
# remember_digestカラムにランダムな文字列を代入する
end
end
###演習
1.
コンソールを開き、データベースにある最初のユーザーを変数userに代入してください。その後、そのuserオブジェクトからrememberメソッドがうまく動くかどうか確認してみましょう。また、remember_tokenとremember_digestの違いも確認してみてください。
>> user = User.first
(1.5ms) SELECT sqlite_version(*)
User Load (2.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "a", email: "abc@def.com", created_at: "2021-10-05 04:11:41", updated_at: "2021-10-05 04:11:41", password_digest: [FILTERED], remember_digest: nil>
>> user.remember
(0.1ms) begin transaction
User Update (5.4ms) UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ? [["updated_at", "2021-10-09 03:47:02.951086"], ["remember_digest", "$2a$12$8L6UrQUhfo/Fru50d81U5.LY3pIkuWZB2Ho1WhfXTUrkVGSZwEhYi"], ["id", 1]]
(6.9ms) commit transaction
=> true
>> user.remember_token
=> "ACeVmrqEjrFZOM8IsV6GNA"
>> user.remember_digest
=> "$2a$12$8L6UrQUhfo/Fru50d81U5.LY3pIkuWZB2Ho1WhfXTUrkVGSZwEhYi"
リスト 9.3では、明示的にUserクラスを呼び出すことで、新しいトークンやダイジェスト用のクラスメソッドを定義しました。実際、User.new_tokenやUser.digestを使って呼び出せるようになったので、おそらく最も明確なクラスメソッドの定義方法であると言えるでしょう。しかし実は、より「Ruby的に正しい」クラスメソッドの定義方法が2通りあります。1つはややわかりにくく、もう1つは非常に混乱するでしょう。テストスイートを実行して、ややわかりにくいリスト 9.4の実装でも、非常に混乱しやすいリスト 9.5の実装でも、いずれも正しく動くことを確認してみてください。ヒント: selfは、通常の文脈ではUser「モデル」、つまりユーザーオブジェクトのインスタンスを指しますが、リスト 9.4やリスト 9.5の文脈では、selfはUser「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります。
user.rb
.
.
.
def self.digest(string)
# 渡された文字列のハッシュ値を返す
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
# fixture用のパスワードを作成します
# stringはハッシュ化する文字列
# costはコストパラメータと呼ばれる値
# ハッシュを算出するための計算コストを指定
end
# ランダムなトークンを返す
def self.new_token
SecureRandom.urlsafe_base64
# ランダムな文字列を生成
end
.
.
.
テスト
ubuntu:~/environment/sample_app (advanced-login) $ rails t
Running via Spring preloader in process 5946
Started with run options --seed 45212
24/24: [============================] 100% Time: 00:00:03, Time: 00:00:03
Finished in 3.46418s
24 tests, 61 assertions, 0 failures, 0 errors, 0 skips
.
.
.
class << self
# selfはユーザーオブジェクトを指す
# 何かしら代入、継承などをしている。
# なので省略できる
def digest(string)
# 渡された文字列のハッシュ値を返す
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
# fixture用のパスワードを作成します
# stringはハッシュ化する文字列
# costはコストパラメータと呼ばれる値
# ハッシュを算出するための計算コストを指定
end
# ランダムなトークンを返す
def new_token
SecureRandom.urlsafe_base64
# ランダムな文字列を生成
end
end
.
.
.
ubuntu:~/environment/sample_app (advanced-login) $ rails t
Running via Spring preloader in process 6604
Started with run options --seed 45054
24/24: [============================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.20782s
24 tests, 61 assertions, 0 failures, 0 errors, 0 skips
確認 成功