ぞえの技術めも

Ruby on Rails勉強中

【88日目】【1日20分のRailsチュートリアル】【第8章】2つの目立たないバグの原因を理解する

Ruby on Railsチュートリアル(第3版)

今日は「8.4.4 2つの目立たないバグ」から。

8.4.4 2つの目立たないバグ

実は小さなバグが2つ残っています。2つのバグは互いに強く関連しています。

へ~。まとめるとこんな感じらしい。

(1)同じサイトに複数のウィンドウ(もしくはタブ)でログインしており、片方のウィンドウでログアウト→もう片方のウィンドウでログアウトしたときにエラー発生

(2)同じサイトに複数のブラウザ(ChromeFirefoxなど)でログインしており、片方のブラウザでログアウト(①)→もう片方のブラウザでログアウトせずにブラウザ終了(②)→再度同じページを開く(③)とエラー発生

(1)はなんとなく分かった。

(2)がなんだか分かりにくいけど、

①でログアウトしたときにUserモデルのデータベースに保存してある記憶トークン(remember_token)が削除される
 ↓
②でブラウザ終了したときに一時セッションに保存していたuserが削除される
 ↓
③でページ開いたときにcurrent_userを取りに行って、ログアウトしてないブラウザのcookiesは残っているので
Userモデルのデータベースに記憶トークンを取りに行くけど①で削除されているので例外が発生する

ってことらしい。ふむふむ。

バグの理解に時間がかかったので今日はここまで。

今日の作業時間は【30分】

次は「8.4.4 2つの目立たないバグ」のテストを書くところから。

【87日目】【1日20分のRailsチュートリアル】【第8章】ログアウトで保持しているユーザー情報を削除する

Ruby on Railsチュートリアル(第3版)

今日は「8.4.3 ユーザーを忘れる」から。

8.4.3 ユーザーを忘れる

ユーザーがログアウトできるようにするために、ユーザーを記憶するためのメソッドと同様の方法で、ユーザーを忘れるためのメソッドを定義します。

どうでもいいところなんだけど「ユーザーを忘れる」ていう表現になんか違和感がある、、、「保持しているユーザー情報を削除する」ってことだよね。
「記憶」トークンだから「忘れる」という表現になるのかな。

Userモデルにremember_digestnilで更新するメソッドを追加。

app/models/user.rb

  # ユーザーログインを破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end

終了するには、forgetヘルパーメソッドを追加してlog_outヘルパーメソッドから呼び出します。

forgetヘルパーメソッドでcookiesの情報を削除する。

app/helpers/sessions_helper.rb

  # 永続的セッションを破棄する
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # 現在のユーザーがログアウトする
  def log_out
    forget(current_user)
    :

ログアウト処理を実装できたので実動作見てみよう。

サーバーを起動して

$ rails server -b $IP -p $PORT

テストアカウントでログイン。

f:id:kt_zoe:20161209123717p:plain

cookieが保存されていることを確認。

f:id:kt_zoe:20161209123820p:plain

ログアウトするとremember_tokencookieが削除されていることを確認。

f:id:kt_zoe:20161209123905p:plain

ちゃんとログアウトできるようになりました。

今日の作業時間は【19分】

次は「8.4.4 2つの目立たないバグ」から。

【86日目】【1日20分のRailsチュートリアル】【第8章】ログイン状態の保持機能を追加する

Ruby on Railsチュートリアル(第3版)

今日は「8.4.2 ログイン状態の保持」のトークンが記憶ダイジェストと一致するか確認するから。

8.4.2 ログイン状態の保持

攻撃者が仮に両方のcookiesを奪い取ることに成功したとしても、本物のユーザーがログアウトするとログインできないようになっています。

記憶トークンとユーザーIDのcookiesを奪い取られてもログインできないらしい。

渡されたトークンがユーザーの記憶ダイジェストと一致することを確認します。

secure_passwordのソースコードを参考にして最終的には下記のような形に。

is_password?がどういうメソッドなのかよく分からないなぁ…。

BCrypt::Password.new(remember_digest).is_password?(remember_token)

上記を踏まえUserモデルにトークンをチェックするメソッドを追加。

app/models/user.rb

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

ログインするときの処理にrememberメソッドを追加。

app/controllers/sessions_controller.rb

    :
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user
      redirect_to user
    else
    :

Sessionヘルパーにrememberメソッドを作成。

app/helpers/sessions_helper.rb

  # ユーザーを永続的セッションに記憶する
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

永続セッションの場合は、session[:user_id]が存在すれば一時セッションからユーザーを取り出し、それ以外の場合はcookies[:user_id]からユーザーを取り出して、対応する永続セッションにログインする必要があります。

状況によって一時セッションとcookiesを使い分けるようにcurrent_userメソッドを修正。

app/helpers/sessions_helper.rb

  # 記憶トークンcookieに対応するユーザーを返す
  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

ブラウザのcookiesを削除する手段が未実装なので (20年待てば消えますが)、ユーザーがログアウトできません。

ログアウトボタン押してもcookiesが削除されない=ログアウトできないのでテストがNGとなることを確認。

$ bundle exec rake test
24 tests, 59 assertions, 1 failures, 0 errors, 0 skips

実際にcookiesに保存されるかどうかはまだ動作見てない。それは追々。
なんか難しくなってきたなぁ、という印象。。。「記憶トークン」という単語が見慣れないからかも?

今日の作業時間は【32分】

次は「8.4.3 ユーザーを忘れる」から。

【85日目】【1日20分のRailsチュートリアル】【第8章】記憶トークンとユーザーIDをcookiesに保存する

Ruby on Railsチュートリアル(第3版)

今日は「8.4.2 ログイン状態の保持」から。

8.4.2 ログイン状態の保持

個別のcookiesは、ひとつのvalue (値) と、オプションのexpires (有効期限) からできています。

valueには記憶トークンの値を設定すればいいらしい。

また、cookiesの有効期限を20年後に設定するのははよく使われる手法とのこと。
Railsには20年後に期限切れになるcookiesを設定できるpermanentメソッドがあるそう。へ~。

cookies.permanent[:remember_token] = remember_token

次はユーザーIDをcookiesに保存。
sessionメソッドを同じ感じにすると下記の通り。

cookies[:user_id] = user.id

しかしこのままではIDが生のテキストとしてcookiesに保存されてしまうので、アプリケーションのcookiesの形式が見え見えになってしまい、(中略)
これを避けるために、署名付きcookieを使用します。

署名付きcookieにはsignedメソッドを使う。

cookies.signed[:user_id] = user.id

これにさっきの有効期限20年cookiesを設定できるpermanentメソッドを追加するとこうなる。

cookies.permanent.signed[:user_id] = user.id

cookiesを設定すると、以後のページのビューで以下のようにしてcookiesからユーザーを取り出せるようになります。

User.find_by(id: cookies.signed[:user_id])

ふーむ、cookies.signed[:user_id]でユーザーIDのcookiesの暗号が解除されるらしい。

記憶トークンとユーザーIDをcookiesに保存する方法を学習した。
色々読み込んでたら時間かかったので今日はここまで。

今日の作業時間は【20分】

次は「8.4.2 ログイン状態の保持」のトークンが記憶ダイジェストと一致するか確認するところから。

【84日目】【1日20分のRailsチュートリアル】【第8章】セッション永続化のために記憶トークンを生成する

Ruby on Railsチュートリアル(第3版)

今日は「8.4 [このアカウント設定を保存する]」から。

8.4 [このアカウント設定を保存する]

本節では、ユーザーログインをデフォルトで保持するように変更し、
(中略)
前者はGitHubやBitbucketで、後者はFacebookTwitterでそれぞれ採用されています。

商用向けサイトでも使える方法を学べるのいいね。

8.4.1 記憶トークンと暗号化

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

セッションは簡単だったけど永続的にしてセキュリティも確保しようとすると色々しないと駄目なんだな。。。

それでは最初に、必要となるremember_digest属性をUserモデルに追加します。

マイグレーションを作成してそのまま実行。
データベースにremember_digest属性ができました。

$ rails generate migration add_remember_digest_to_users remember_digest:string
      invoke  active_record
      create    db/migrate/20161206021530_add_remember_digest_to_users.rb
$ bundle exec rake db:migrate

ここで、記憶トークンとして何を使用するかを決める必要があります。

ランダムな文字列生成にSecureRandomモジュールのurlsafe_base64メソッドを使うのがいいらしい。

app/models/user.rb

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

有効なトークンとそれに関連するダイジェストを作成できるようにします。

さっき追加したUser.new_tokenを使ってトークンのダイジェストをデータベースに保存する。

app/models/user.rb

class User < ActiveRecord::Base
  attr_accessor :remember_token
  :
  # 永続的セッションで使用するユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end

トークンをどう使うのかしっかりイメージできてないけどこれからかな。

今日の作業時間は【29分】

次は「8.4.2 ログイン状態の保持」から。

【83日目】【1日20分のRailsチュートリアル】【第8章】ログアウト機能を追加する

Ruby on Railsチュートリアル(第3版)

今日は「8.3 ログアウト」から。

8.3 ログアウト

ユーザーセッションを破棄するための有効なアクションをコントローラで作成するだけで済みます。

ユーザーセッションを破棄するlog_outメソッドをSessionヘルパーに追加。

app/helpers/sessions_helper.rb

  :
  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

Sessionのdestroyアクションにてlog_outメソッドを呼び出す。

app/controllers/sessions_controller.rb

  def destroy
    log_out
    redirect_to root_url
  end

ログアウト機能をテストするために、リスト8.20のユーザーログインのテストに手順を若干追加します。

ログアウトを実施して、ルートURLにリダイレクトされること、ログイン用リンクが表示されること、ログアウト用リンクとプロフィールリンクが表示されないことを確認する。

test/integration/users_login_test.rb

  :
  test "login with valid information followed by logout" do
    get login_path
    post login_path, session: { email: @user.email, password: 'password' }
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end

テスト実行して問題ないことを確認。

$ bundle exec rake test
24 tests, 61 assertions, 0 failures, 0 errors, 0 skips

ログアウト処理はセッションからユーザー情報削除するのがメインだしそんなに難しくない印象。

今日の作業時間は【17分】

次は「8.4 [このアカウント設定を保存する]」から。

【82日目】【1日20分のRailsチュートリアル】【第8章】ユーザー登録時にログインするようにする

Ruby on Railsチュートリアル(第3版)

今日は「8.2.5 ユーザー登録時にログイン」から。

8.2.5 ユーザー登録時にログイン

以上で認証システムが動作するようになりましたが、今のままでは、登録の終わったユーザーがデフォルトではログインしていないので、
(中略)
ユーザー登録中にログインを済ませておくことにします。

Webサービスの中でもユーザー新規作成時にログインしてくれるやつとしてくれないやつあるよね。
私も登録時にログインしてくれるやつの方がいいなぁ。

app/controllers/users_controller.rb

      :
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      :

log_inメソッドを追加しただけ。

リスト8.22の動作をテストするために、リスト7.26のテストに1行追加して、ユーザーがログイン中かどうかをチェックします。
そのために、リスト8.15で定義したlogged_in?ヘルパーメソッドとは別に、is_logged_in?ヘルパーメソッドを定義しておくと便利です。

前追加したlogged_in?ヘルパーメソッドはテストから呼び出せないらしいのでテスト用にヘルパーメソッドを作成する。

test/test_helper.rb

  # テストユーザーがログインしていればtrueを返す
  def is_logged_in?
    !session[:user_id].nil?
  end

ヘルパーメソッドを使ってテストを追加。

test/integration/users_signup_test.rb

  test "valid signup information" do
    :
    assert is_logged_in?
  end

テスト実行して問題ないことを確認。

$ bundle exec rake test
24 tests, 55 assertions, 0 failures, 0 errors, 0 skips

機能的には一行追加するだけって簡単だなぁ。

今日の作業時間は【16分】

次は「8.3 ログアウト」から。