Try T.M Engineer Blog

多摩市で生息するエンジニアが「アウトプットする事は大事だ」と思って始めたブログ

【Ruby on Railsチュートリアル(第4版)】第9章 発展的なログイン機構(演習と解答)

はじめに

このブログ記事は、私(Kodak)自身のRailsの勉強記録として書いています。
Ruby on Railsチュートリアル』の演習と解答をもくもくと書いているだけの記事なので、興味の無い方は軽くスルーしてあげてください。他にも、まだ『Railsチュートリアル』の演習に挑戦していない方、これから『Railsチュートリアル』をやるぞ!という方は(演習の解答の)ネタバレになりますので、スルーしてください。

Ruby on Rails チュートリアルとは?

Ruby業界でRailsを使い始めるなら、まず最初に初めるRails入門サイトです。
電子書籍版は有料ですが、Webサイトにあるオンライン版は無料なので、誰でも読む(Railsにチャレンジする)事ができます。
railstutorial.jp
全部で14章あり、かなりのボリュームですが、これを読む事でRailsの基礎を学ぶ事ができ、ちょっとしたWebアプリケーションを作れるレベルにはなれる?とのこと。
各章毎に演習問題が複数あり、これを解いていく事でRailsへの理解を深めていく事ができる様になっています。

環境について

Ruby 2.5.0-dev
Rails 5.1.4
・バージョン管理ツール:GitHub(https://github.com/Kodak4400)

演習と解答

9.1.1 記憶トークンと暗号化<演習>
1. コンソールを開き、データベースにある最初のユーザーを変数userに代入してください。その後、そのuserオブジェクトからrememberメソッドがうまく動くかどうか確認してみましょう。また、remember_tokenとremember_digestの違いも確認してみてください。
【解答】以下の通り。

$ rails console --sandbox
Loading development environment in sandbox (Rails 5.1.4)
Any modifications you make will be rolled back on exit
irb(main):001:0> user = User.first
  User Load (0.4ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2018-08-12 15:23:47", updated_at: "2018-08-12 15:23:47", password_digest: "$2a$10$j8YTAFtYA/b8uo0q0gqNX.9Zox3VCDNwhYkjMd6ET87...", remember_digest: nil>
irb(main):002:0> user
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2018-08-12 15:23:47", updated_at: "2018-08-12 15:23:47", password_digest: "$2a$10$j8YTAFtYA/b8uo0q0gqNX.9Zox3VCDNwhYkjMd6ET87...", remember_digest: nil>
irb(main):003:0> user.remember
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.3ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2018-08-28 12:14:56.339679"], ["remember_digest", "$2a$10$JXbVWHZy9Y649zF9V5UgDOwueqLWMDsiPdoEUw2SLR3kN5qr/4aHC"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
irb(main):004:0> user.remember_token
=> "nCgSjbDAyJkTft85xVTCCA"
irb(main):005:0> user.remember_digest
=> "$2a$10$JXbVWHZy9Y649zF9V5UgDOwueqLWMDsiPdoEUw2SLR3kN5qr/4aHC"
irb(main):006:0>

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「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります。
実機操作のため割愛。

9.1.2 ログイン状態の保持<演習>
1. ブラウザのcookieを調べ、ログイン後のブラウザではremember_tokenと暗号化されたuser_idがあることを確認してみましょう。
【解答】以下の通り。
f:id:special-moucom:20180830092424p:plain

2. コンソールを開き、リスト 9.6のauthenticated?メソッドがうまく動くかどうか確かめてみましょう。
【解答】以下の通り。

$ rails console --sandbox
Loading development environment in sandbox (Rails 5.1.4)
Any modifications you make will be rolled back on exit
irb(main):001:0> user = User.first
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2018-08-12 15:23:47", updated_at: "2018-08-28 14:19:43", password_digest: "$2a$10$j8YTAFtYA/b8uo0q0gqNX.9Zox3VCDNwhYkjMd6ET87...", remember_digest: "$2a$10$ngv5/yMYwIknzAmT.xNE/eEcplZiu2PVr7HscyaiAau...">
irb(main):002:0> user
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2018-08-12 15:23:47", updated_at: "2018-08-28 14:19:43", password_digest: "$2a$10$j8YTAFtYA/b8uo0q0gqNX.9Zox3VCDNwhYkjMd6ET87...", remember_digest: "$2a$10$ngv5/yMYwIknzAmT.xNE/eEcplZiu2PVr7HscyaiAau...">
irb(main):003:0> user.remember
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.4ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2018-08-28 14:25:05.418184"], ["remember_digest", "$2a$10$g6KH9a8pP4rNF6n.aIFZ..HM/ShRzGLe0Fb.pkoUxtKn6u0m5isZ6"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
irb(main):004:0> user.remember_token
=> "8SqCVjnPAg7x8tGVE0UwHQ"
irb(main):005:0> user.authenticated?(user.remember_token)
=> true
irb(main):006:0>

9.1.3 ユーザーを忘れる<演習>
1. ログアウトした後に、ブラウザの対応するcookiesが削除されていることを確認してみましょう。
実機操作のため割愛。

9.1.4 2つの目立たないバグ<演習>
1. リスト 9.16で修正した行をコメントアウトし、2つのログイン済みのタブによるバグを実際に確かめてみましょう。まず片方のタブでログアウトし、その後、もう1つのタブで再度ログアウトを試してみてください。
【解答】実機操作のため割愛。

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

3. 上のコードでコメントアウトした部分を元に戻し、テストスイートが red から greenになることを確認しましょう。
【解答】以下の通り。

### app/models/user.rb
class User < ApplicationRecord

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  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
### app/controllers/sessions_controller.rb 
class SessionsController < ApplicationController

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end
$ rails test
Finished in 0.754003s, 33.1564 runs/s, 91.5116 assertions/s.
25 runs, 69 assertions, 0 failures, 0 errors, 0 skips
$

9.2 [Remember me]チェックボックス<演習>
1. ブラウザでcookies情報を調べ、[remember me] をチェックしたときに意図した結果になっているかどうかを確認してみましょう。
実機操作のため割愛。

2. コンソールを開き、三項演算子を使った実例を考えてみてください (コラム 9.2)。
【解答】以下の通り。

$ rails console --sandbox
Loading development environment in sandbox (Rails 5.1.4)
Any modifications you make will be rolled back on exit
irb(main):001:0> def your_name?(name)
irb(main):002:1>  name == 'Kodak' ? "yes" : "no"
irb(main):003:1> end
=> :your_name?
irb(main):004:0> your_name?("Kodak")
=> "yes"
irb(main):005:0> your_name?("Magikarp")
=> "no"
irb(main):006:0>

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

### app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  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

end
class UsersLoginTest < ActionDispatch::IntegrationTest

  test "login with remembering" do
    log_in_as(@user, remember_me:'1')
    assert_equal cookies['remember_token'], assigns(:user).remember_token
  end

end
$ rails test
Finished in 0.706577s, 38.2124 runs/s, 101.8997 assertions/s.

27 runs, 72 assertions, 0 failures, 0 errors, 0 skips

9.3.2 [Remember me]をテストする<演習> 1. リスト 9.33にあるauthenticated?の式を削除すると、リスト 9.31の2つ目のテストで失敗することを確かめてみましょう (このテストが正しい対象をテストしていることを確認してみましょう)。 【解答】以下の通り。

### app/helpers/sessions_helper.rb
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
Failure:
SessionsHelperTest#test_current_user_returns_nil_when_remember_digest_is_wrong [rails-tutorial/sample_app/test/helpers/sessions_helper_test.rb:17]:
Expected #<User id: 762146111, name: "Michael Example", email: "michael@example.com", created_at: "2018-08-29 13:39:31", updated_at: "2018-08-29 13:39:31", password_digest: "$2a$04$m6OP7WuMes8ezEStQDXfJenCq3N8XncZGmN4sgJ8Php...", remember_digest: "$2a$04$ZqqQuHUiHVdszcZNt2ZTDekJjOVrZgtmAWNJ5JjTQl0..."> to be nil.