おぴよの気まぐれ日記

おぴよの気まぐれ日記

岡山やプログラミング、ファッションのこと、子育てや人生、生き方についての備忘録。

30歳まで残り2年の僕は人生を変えるためにRailsチュートリアルを始めようと思う(第9章)

こんにちは。opiyoです。

今回は、第9章をやっていきます。

第9章は画面からユーザーを登録する方法です。

9.1.1 演習 記憶トークンと暗号化

9.1.1.1

<問題>コンソールを開き、データベースにある最初のユーザーを変数userに代入してください。その後、そのuserオブジェクトからrememberメソッドがうまく動くかどうか確認してみましょう。また、remember_tokenとremember_digestの違いも確認してみてください。

<回答>

* user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-03 07:19:07", updated_at: "2017-07-03 07:40:01", password_digest: "$2a$10$pQp3dmB7GUGAw/0gkC8KKuH7iS2R5afnYNlTmqg/mT5...", remember_digest: nil>
irb(main):012:0> user.remember
   (0.1ms)  begin transaction
  SQL (0.3ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", 2017-08-18 01:28:49 UTC], ["remember_digest", "$2a$10$G7Gzgfj9Ypt7BnwJVDAhfuzbxhxMXv4f67/04hjZbIT.O/BoNA.BO"], ["id", 1]]
   (1.5ms)  commit transaction
=> true
irb(main):013:0> user
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-03 07:19:07", updated_at: "2017-08-18 01:28:49", password_digest: "$2a$10$pQp3dmB7GUGAw/0gkC8KKuH7iS2R5afnYNlTmqg/mT5...", remember_digest: "$2a$10$G7Gzgfj9Ypt7BnwJVDAhfuzbxhxMXv4f67/04hjZbIT...">
irb(main):014:0> user.remember_token
=> "r_pj-i3RnMV0jGD7U9VdEg"
irb(main):015:0> user.remember_digest
=> "$2a$10$G7Gzgfj9Ypt7BnwJVDAhfuzbxhxMXv4f67/04hjZbIT.O/BoNA.BO"

9.1.1.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「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります。

<回答>

  # 渡された文字列のハッシュ値を返す
  def self.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
   
  # ランダムなトークンを返す
  def self.new_token
    SecureRandom.urlsafe_base64
  end

問題なし

  class << self
    # 渡された文字列のハッシュ値を返す
    def digest(string)
      cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                    BCrypt::Engine.cost
      BCrypt::Password.create(string, cost: cost)
    end
      
    # ランダムなトークンを返す
    def new_token
      SecureRandom.urlsafe_base64
    end
  end

問題なし

9.1.2 演習 ログイン状態の保持

9.1.2.1

<問題>ブラウザのcookieを調べ、ログイン後のブラウザではremember_tokenと暗号化されたuser_idがあることを確認してみましょう。

<回答>

Chromeのディベロッパーツールを使って確認。

remember_token:_T_91tSHmmZtIf6GLa3Pzg
user_id:5d0aa7d03c456ef4864c32703d61611e49364eaa

9.1.2.2

<問題>コンソールを開き、リスト 9.6のauthenticated?メソッドがうまく動くかどうか確かめてみましょう。

<回答>

* u = User.last
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "haku", email: "haku@co.jp", created_at: "2017-08-18 01:58:33", updated_at: "2017-08-18 02:02:05", password_digest: "$2a$10$e9Rq/VoCQN8j6.7G0CYYG.dOlVAMhhdGOidroqcUgtc...", remember_digest: "$2a$10$HXE4Ts.phrwUZUZXMJJh4ud2MsVMpEN32GT7fK/ZvjD...">
irb(main):010:0> token = "_T_91tSHmmZtIf6GLa3Pzg"
=> "_T_91tSHmmZtIf6GLa3Pzg"
irb(main):011:0> u.authenticated?(token)
=> true

9.1.3 演習 ユーザーを忘れる

9.1.3.1

<問題>ログアウトした後に、ブラウザの対応するcookiesが削除されていることを確認してみましょう。

<回答> Chromeのディベロッパーツールを使ってcookiesが削除されていることを確認。

9.1.4 演習 2つの目立たないバグ

9.1.4.1

<問題>リスト 9.16で修正した行をコメントアウトし、2つのログイン済みのタブによるバグを実際に確かめてみましょう。まず片方のタブでログアウトし、その後、もう1つのタブで再度ログアウトを試してみてください。

<回答>

エラーになることを確認!

NoMethodError in SessionsController#destroy
undefined method `forget' for nil:NilClass

9.1.4.2

<問題>リスト 9.19で修正した行をコメントアウトし、2つのログイン済みのブラウザによるバグを実際に確かめてみましょう。まず片方のブラウザでログアウトし、もう一方のブラウザを再起動してサンプルアプリケーションにアクセスしてみてください。

<回答> もう一方のブラウザを再起動してサンプルアプリケーションにアクセスできちゃうことを確認!

9.1.4.3

<問題>上のコードでコメントアウトした部分を元に戻し、テストスイートが red から greenになることを確認しましょう。

<回答> テストが通ることを確認!

9.2 演習 [Remember me] チェックボックス

9.2.1

<問題>ブラウザでcookies情報を調べ、[remember me] をチェックしたときに意図した結果になっているかどうかを確認してみましょう。

<回答>

ログインが保持されていることを確認しました。 - [remember me] へのチェックあり:「remember_token」、「user_id」が保存される - [remember me] へのチェックなし:「remember_token」、「user_id」が保存されない

9.2.2

<問題>コンソールを開き、三項演算子を使った実例を考えてみてください (コラム 9.2)。

<回答>

> result = 90
=> 90
irb(main):018:0> result > 80 ? "合格":"不合格"
=> "合格"
irb(main):019:0> result = 60
=> 60
irb(main):020:0> result > 80 ? "合格":"不合格"
=> "不合格"

※僕の環境(Ruby 2.3.3)だと、日本語入力すると化ける。もし同じ現象になる場合はRubyのバージョンを変更して見てください。

$ ruby -v
ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin15]
rh0257:railstutorial_9 taku$ rbenv versions
  system
  2.1.5
  2.1.6
* 2.3.3 (set by /Users/taku/rails/railstutorial/railstutorial_9/.ruby-version)
  2.3.4
  2.4.1
rh0257:railstutorial_9 taku$ rbenv local 2.3.4
rh0257:railstutorial_9 taku$ ruby -v
ruby 2.3.4p301 (2017-03-30 revision 58214) [x86_64-darwin15]

# インストールする場合
$ rbenv install --list
$ rbenv install 2.3.4

9.3.1 演習 [Remember me] ボックスをテストする

9.3.1.1

<問題>リスト 9.25の統合テストでは、仮想のremember_token属性にアクセスできないと説明しましたが、実は、assignsという特殊なテストメソッドを使うとアクセスできるようになります。コントローラで定義したインスタンス変数にテストの内部からアクセスするには、テスト内部でassignsメソッドを使います。このメソッドにはインスタンス変数に対応するシンボルを渡します。例えばcreateアクションで@userというインスタンス変数が定義されていれば、テスト内部ではassigns(:user)と書くことでインスタンス変数にアクセスできます。本チュートリアルのアプリケーションの場合、Sessionsコントローラのcreateアクションでは、userを (インスタンス変数ではない) 通常のローカル変数として定義しましたが、これをインスタンス変数に変えてしまえば、cookiesにユーザーの記憶トークンが正しく含まれているかどうかをテストできるようになります。このアイデアに従ってリスト 9.27とリスト 9.28の不足分を埋め (ヒントとして?やFILL_INを目印に置いてあります)、[remember me] チェックボックスのテストを改良してみてください。

<回答>

class SessionsController < ApplicationController
  def new
  end

  def create
    @user = User.find_by(email: params[:session][:email].downcase) # ローカル変数`user`に@を付けてインスタンス変数へ
    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

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

(略)

  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_equal cookies['remember_token'], assigns(:user).remember_token # インスタンス変数に設定された`remember_token`で比較する
  end

end

9.3.2 演習 [Remember me] をテストする

9.3.2.1

<問題>リスト 9.33にあるauthenticated?の式を削除すると、リスト 9.31の2つ目のテストで失敗することを確かめてみましょう (このテストが正しい対象をテストしていることを確認してみましょう)。

<回答>

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]) 
      if user
        log_in user
        @current_user = user
      end
    end
  end

この状態でテストすると失敗することを確認。

メモ

  • 記憶トークン (remember token) を生成し、cookiesメソッドによる永続的cookiesの作成
  • 安全性の高い記憶ダイジェスト (remember digest) によるトークン認証にこの記憶トークンを活用