らくだ🐫にもできるRailsチュートリアル|9.1

9章でやる事
(任意で)ユーザーのログイン情報を記憶しておけるようにする
→remember me機能
永続cookieを使ってremember me機能を実現

9.1 Remember me 機能

ブラウザを閉じた後でもログイン状態を有効にする[remember me] 機能を実装していく

この機能を使うと、ユーザーが明示的にログアウトを実行しない限り、ログイン状態を維持することができるようになります。

9.1.1 記憶トークンと暗号化

8.2で行ったsessionメソッドを使ってユーザーIDを保存する方法ではブラウザを閉じるとユーザー情報が消えてしまう。

本節では、セッションの永続化の第一歩として記憶トークン (remember token) を生成し、cookiesメソッドによる永続的cookiesの作成や、安全性の高い記憶ダイジェスト (remember digest) によるトークン認証にこの記憶トークンを活用します。

トークンとはなんぞ

パスワード→ユーザーが作成・管理する情報
トークン→コンピューターが作成・管理する情報
パスワードの平文と同じような秘密情報

永続的セッション作成作成の手順

  1. 記憶トークンにはランダムな文字列を生成して用いる。
  2. ブラウザのcookiesにトークンを保存するときには、有効期限を設定する。
  3. トークンはハッシュ値に変換してからデータベースに保存する。
  4. ブラウザのcookiesに保存するユーザーIDは暗号化しておく。
  5. 永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。

remember_digest属性を追加

remember_digest属性をUserモデルに追加する

記憶トークンとして何を使うか

基本的には長くてランダムな文字列であれば何でもOK!
本文ではRuby標準ライブラリのSecureRandomモジュールにあるurlsafe_base64メソッドを利用する
→いろいろと都合が良い(本文参照)

ユーザーを記憶する仕組みを作る準備

記憶トークンを作成→ダイジェストに変換したものをDBに保存
なので、記憶トークンを作成するメソッドを定義する

ダイジェストに変換するためのメソッドはfixture用に作った(digestメソッド)が利用できる

user.rememberメソッドを作成

記憶トークンとユーザーを関連付け、トークンに対応する記憶ダイジェストをDBに保存する。
Userモデルにはremember_digest属性を追加したがremember_token属性はまだ追加していない。

user.remember_tokenメソッドを使ってトークンにアクセスできるようにし、かつ、トークンをデータベースに保存せずに実装する必要があります。

パスワードの実装と同じ手法で実装する
パスワード実装の際は仮想のpassword属性はhas_secure_passwordメソッドで自動的に作成されたけど
remember_tokenの仮想の属性は自分で定義する必要がある

attr_accessorを使って「仮想の」属性を作成

attr_accessor→RoRT本文らくだ🐫にもできるRailsチュートリアル|4.4(と4.5)

上記を考慮して実際にコードを書いていく

最初にUser.new_tokenで記憶トークンを作成し、続いてUser.digestを適用した結果で記憶ダイジェストを更新します。

省略されるselfと省略できないself

上記の

selfの有無によって self.remember_token(インスタンス変数) remember_token(メソッド内の変数)になっちゃうから
30行目の(remember_token)self.つけないとじゃない?っていう話が出まして
この二つのself.remember_tokenとremember_tokenは何が違ってどう使い分けられてるのって思って調べたところ
本来はselfついてるけど省略されているよ!ということのようです。

演習

  1. コンソールを開き、データベースにある最初のユーザーを変数userに代入してください。その後、そのuserオブジェクトからrememberメソッドがうまく動くかどうか確認してみましょう。また、remember_tokenとremember_digestの違いも確認してみてください。
  2. リスト 9.3では、明示的にUserクラスを呼び出すことで、新しいトークンやダイジェスト用のクラスメソッドを定義しました。実際、User.new_tokenやUser.digestを使って呼び出せるようになったので、おそらく最も明確なクラスメソッドの定義方法であると言えるでしょう。しかし実は、より「Ruby的に正しい」クラスメソッドの定義方法が2通りあります。1つはややわかりにくく、もう1つは非常に混乱するでしょう。テストスイートを実行して、ややわかりにくいリスト 9.4の実装でも、非常に混乱しやすいリスト 9.5の実装でも、いずれも正しく動くことを確認してみてください。ヒント: selfは、通常の文脈ではUser「モデル」、つまりユーザーオブジェクトのインスタンスを指しますが、リスト 9.4やリスト 9.5の文脈では、selfはUser「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります

selfとはなんぞ


クラスメソッドを定義するもの
クラス名.メソッド名は明確な定義方法ではあるけれど

より「Ruby的に正しい」クラスメソッドの定義方法

と言う事のようです

9.1.2 ログイン状態の保持

user.rememberメソッドが動作するようになったので、ユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存して、永続セッションを作成する準備ができました。

cookiesメソッドを用いて永続セッションを作成

cookiesメソッド

sessionメソッドと同様にハッシュとして扱える
1つのvalue (値) とオプションのexpires (有効期限)から出来ている(有効期限は省略も可能)
参考記事

署名付きcookie

bcryptで確認する

ここまでの参考コードをもとにして記憶トークンと記憶ダイジェストを比較する

authenticated?メソッドのローカル変数として定義したremember_tokenは、リスト 9.3のattr_accessor :remember_tokenで定義したアクセサとは異なる点に注意してください (リスト 9.6)。今回の場合、is_password?の引数はメソッド内のローカル変数を参照しています。もう1つ、remember_digestの属性の使い方にもご注目ください。この使い方はself.remember_digestと同じであり、すなわち第6章のnameやemailの使い方と同じになります。

remember_digestの属性はデータベースのカラムに対応している→Active Recordによって簡単に取得したり保存したり出来る

rememberヘルパーメソッドを追加

ログインしてユーザーを保持

ログインするユーザーはブラウザで有効な記憶トークンを得られるようになった!
→current_userメソッドが正常に作動しなくなっている!!(一時セッションしか扱っていないため)

上記を参考にcurrent_userヘルパーを定義

アプリケーションに現在残された問題はあと1つだけです。ブラウザのcookiesを削除する手段が未実装なので (20年待てば消えますが)、ユーザーがログアウトできません。これは当然テストスイートでキャッチすべき問題であり、redにならなければなりません。

→9.1.3で修正

演習

  1. ブラウザのcookieを調べ、ログイン後のブラウザではremember_tokenと暗号化されたuser_idがあることを確認してみましょう
  2. コンソールを開き、リスト 9.6のauthenticated?メソッドがうまく動くかどうか確かめてみましょう。

動作確認のみにて省略

9.1.3 ユーザーを忘れる

ログアウトの機能を作る

ユーザーを記憶するためのメソッドと同様の方法で、ユーザーを忘れるためのメソッドを定義します。このuser.forgetメソッドによって、user.rememberが取り消されます。具体的には、記憶ダイジェストをnilで更新します

上記のforgetメソッドを使って永続セッションからログアウトするヘルパーメソッドを定義する
forgetヘルパーメソッドの新規作成とlog_outヘルパーメソッドへの追加

演習

動作確認のみにて省略

9.1.4 2つの目立たないバグ

お互いが関連しあったバグが2つ残っている

一つ目のバグ

ユーザーが1つのタブでログアウトし、もう1つのタブで再度ログアウトしようとするとエラーになってしまいます。これは、もう1つのタブで “Log out” リンクをクリックすると、current_userがnilとなってしまうため、log_outメソッド内のforget(current_user)が失敗してしまうからです

ユーザーがログイン中の場合にのみログアウトできるようにすることで回避

2つ目のバグ

2番目の地味な問題は、ユーザーが複数のブラウザ (FirefoxやChromeなど) でログインしていたときに起こります。例えば、Firefoxでログアウトし、Chromeではログアウトせずにブラウザを終了させ、再度Chromeで同じページを開くと、この問題が発生します

→上記の例を具体的に考えてみる

この時点ではまだ問題ない

これにより、下記でハイライトされている2行はfalseとなる

結果、current_userメソッドの結果はにnilになる
↑ここまでFireFox
↓ここからChrome

Chromeを閉じたとき、session[:user_id]はnilになります (これはブラウザが閉じたときに、全てのセッション変数の有効期限が切れるためです)。しかし、cookiesはブラウザの中に残り続けているため、Chromeを再起動してサンプルアプリケーションにアクセスすると、データベースからそのユーザーを見つけることができてしまいます。

下記ハイライト部分でユーザーを見つけることが出来てしまう

その結果上記の10行目部分

残っていたcookiesの情報から取り出したユーザーの情報がuserに代入されている為
2番目の条件式まで評価が進む
→FireFoxでログアウト時remember_digestが削除されているが下記6行目のコードを実行してしまうためエラーが起こる
(bcryptライブラリ内部での例外が発生)

→これを解決するためにremember_digestが存在しないときはfalseを返す処理をauthenticated?に追加する必要がある

テスト開発駆動!

ユーザーログインの統合テストに2番目のウィンドウでログアウトをクリックした状況をシミュレートする

current_userがないために2回目のdelete logout_pathの呼び出しでエラーが発生し、テストスイートは redになります。

テストが成功するようにコードを修正
→logged_in?がtrueの場合に限ってlog_outを呼び出す

2番目の問題についてですが、統合テストで2種類のブラウザをシミュレートするのは正直かなり困難です。その代わり、同じ問題をUserモデルで直接テストするだけなら簡単に行えます。

記憶ダイジェストを持たないユーザーを用意

チェーンメソッドでauthenticated?を呼び出す(記憶トークンが空欄のままなのは記憶トークンが使われる前にエラーが発生するから別になんでもいいからのとこと)

authenticated?メソッドの途中でエラーが発生→RED
これを通すため記憶ダイジェストがnilの場合にfalseを返すようにする

以上で目立たない二つのバグが修正されました🎉

演習

動作確認のみにて省略

まとめとか感想

今回の部分は本当に訳が分からなくて
何度も読み返しつつ行ったり来たりしました挙句
年末年始をはさんだりPCの新調があったりで余計間が空いたりでした。
いまだによくわからなかったりはしてるのだけど後から何度も見直していきたい!🐫

らくだ🐫にもできるRailsチュートリアルとは

「ド」が付く素人のらくだ🐫が勉強するRailsチュートリアルの学習記録です。
自分用に記録していますが、お役に立つことがあれば幸いです。

調べたとはいえらくだ🐫なりの解釈や説明が含まれます。間違っている部分もあるかと思います。そんな所は教えて頂けますと幸いなのですが、このブログにはコメント機能がありません💧お手数おかけしますがTwitterなどでご連絡いただければ幸いです