【88日目】【1日20分のRailsチュートリアル】【第8章】2つの目立たないバグの原因を理解する
今日は「8.4.4 2つの目立たないバグ」から。
8.4.4 2つの目立たないバグ
実は小さなバグが2つ残っています。2つのバグは互いに強く関連しています。
へ~。まとめるとこんな感じらしい。
(1)同じサイトに複数のウィンドウ(もしくはタブ)でログインしており、片方のウィンドウでログアウト→もう片方のウィンドウでログアウトしたときにエラー発生
(2)同じサイトに複数のブラウザ(ChromeとFirefoxなど)でログインしており、片方のブラウザでログアウト(①)→もう片方のブラウザでログアウトせずにブラウザ終了(②)→再度同じページを開く(③)とエラー発生
(1)はなんとなく分かった。
(2)がなんだか分かりにくいけど、
①でログアウトしたときにUserモデルのデータベースに保存してある記憶トークン(remember_token)が削除される
↓
②でブラウザ終了したときに一時セッションに保存していたuserが削除される
↓
③でページ開いたときにcurrent_userを取りに行って、ログアウトしてないブラウザのcookiesは残っているので
Userモデルのデータベースに記憶トークンを取りに行くけど①で削除されているので例外が発生する
ってことらしい。ふむふむ。
バグの理解に時間がかかったので今日はここまで。
今日の作業時間は【30分】。
次は「8.4.4 2つの目立たないバグ」のテストを書くところから。
【87日目】【1日20分のRailsチュートリアル】【第8章】ログアウトで保持しているユーザー情報を削除する
今日は「8.4.3 ユーザーを忘れる」から。
8.4.3 ユーザーを忘れる
ユーザーがログアウトできるようにするために、ユーザーを記憶するためのメソッドと同様の方法で、ユーザーを忘れるためのメソッドを定義します。
どうでもいいところなんだけど「ユーザーを忘れる」ていう表現になんか違和感がある、、、「保持しているユーザー情報を削除する」ってことだよね。
「記憶」トークンだから「忘れる」という表現になるのかな。
Userモデルにremember_digest
をnilで更新するメソッドを追加。
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
テストアカウントでログイン。
cookieが保存されていることを確認。
ログアウトするとremember_token
のcookieが削除されていることを確認。
ちゃんとログアウトできるようになりました。
今日の作業時間は【19分】。
次は「8.4.4 2つの目立たないバグ」から。
【86日目】【1日20分のRailsチュートリアル】【第8章】ログイン状態の保持機能を追加する
今日は「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に保存する
今日は「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章】セッション永続化のために記憶トークンを生成する
今日は「8.4 [このアカウント設定を保存する]」から。
8.4 [このアカウント設定を保存する]
本節では、ユーザーログインをデフォルトで保持するように変更し、
(中略)
前者はGitHubやBitbucketで、後者はFacebookやTwitterでそれぞれ採用されています。
商用向けサイトでも使える方法を学べるのいいね。
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章】ログアウト機能を追加する
今日は「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章】ユーザー登録時にログインするようにする
今日は「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 ログアウト」から。