#発展的なログイン機構
■第9章
remember me
というブラウザを再起動した後でもすぐにログインできる機能を備えていることが一般的になってきた。
永続クッキー (permanent cookies) を使ってこの機能を実現していく。
##9.1 Remember me 機能
本節では、ユーザーのログイン状態をブラウザを閉じた後でも有効にする [remember me] 機能を実装していく。この機能を使えば、ログアウトを実行しない限りログイン状態を維持できる。
YahooとかTwitterみたいな感じなのかな?
###9.1.1 記憶トークンと暗号化
セッションの永続化の第一歩として記憶トークン (remember token) を生成し、cookies
メソッドによる永続的cookiesの作成や、安全性の高い記憶ダイジェスト (remember digest) によるトークン認証にこの記憶トークンを活用。
cookies
メソッドに保存する情報は自動的に安全に保たれるようになっていない。cookiesを永続化するとセッションハイジャックという攻撃を受ける可能性がある。
次の方針で永続的セッションを作成する。
・記憶トークンにはランダムな文字列を生成して用いる。
・ブラウザのcookiesにトークンを保存するときには、有効期限を設定する。
・トークンはハッシュ値に変換してからデータベースに保存する。
・ブラウザのcookiesに保存するユーザーIDは暗号化しておく。
・永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。
記憶トークンとして、Ruby標準ライブラリのSecureRandom
モジュールにあるurlsafe_base64
メソッドを使う。このメソッドは長さ22のランダムな文字列を返す。
rails consoleを立ち上げて使ってみるとこんな感じ。
SecureRandom.urlsafe_base64
=> "XkxMplhn38qjFYlFMA3KiQ"
ユーザーを記憶するために、記憶トークンを作成して、そのトークンをダイジェストに変換したものをデータベースに保存。
新しいトークンを作成するためのnew_token
メソッドを作成。また、記憶トークンをユーザーと関連付け、トークンに対応する記憶ダイジェストをデータベースに保存するuser.remember
メソッドも作成。
class User < ApplicationRecord
attr_accessor :remember_token
.
.
.
.
# ランダムなトークンを返す
def User.new_token
SecureRandom.urlsafe_base64
end
# 永続セッションのためにユーザーをデータベースに記憶する
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
end
###9.1.2 ログイン状態の保持
個別のcookiesは、1つのvalue
(値) と、オプションのexpires
(有効期限) からできている。
記憶トークンと記憶ダイジェストを比較するauthenticated?
メソッドを、Userモデルの中に置く。
# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
その後もいろんなファイルの中身の書き換え。だいぶ何言ってるかわからなくなってきました。。。
###9.1.3 ユーザーを忘れる
ユーザーを忘れるためのメソッドを定義する。
このuser.forget
メソッドによって、user.remember
が取り消される。
以下を追記。
# ユーザーのログイン情報を破棄する
def forget
update_attribute(:remember_digest, nil)
end
永続セッションを終了するために、forget
ヘルパーメソッドを追加してlog_out
ヘルパーメソッドから呼び出す。
# 永続的セッションを破棄する
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
# 現在のユーザーをログアウトする
def log_out
forget(current_user)
session.delete(:user_id)
@current_user = nil
end
###9.1.4 2つの目立たないバグ
バグが2つある
1つ目は、2つタブを開いてる状態でログアウトを2回押したときにアクセスエラーになる。この問題の回避のため、ユーザーがログイン中の場合にのみログアウトさせる。
2つ目は、2つのブラウザでログイン状態で、片方をログアウトしてもう片方はログアウトせずに再起動するとややこしくなる。
1つ目のは統合テストにコードを追記して、コントローラでログイン中の場合のみログアウトするようにする。
def destroy
log_out if logged_in?
redirect_to root_url
end
2番目の問題についてですが、統合テストで2種類のブラウザをシミュレートするのはかなり困難らしい。一応コードは書き換えたものの、ここらへんも解説がイマイチ理解できませんでしたね。。。
##9.2 [Remember me] チェックボックス
ログインフォームにチェックボックスを追加するところから始める。ヘルパーメソッドで作成できる。正常に動作するためにはラベルの内側に配置する必要ある。
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
check
とinline
はCSSクラス。
ログインフォームの編集が終わったので、チェックボックスがオンのときにユーザーを記憶し、オフのときには記憶しないようにする。
params[:session][:remember_me]
でチェックボックスのONOFFを取り出せる。
##9.3[Remember me] のテスト
動作をテストで確認できるようにしておくことが重要。
###9.3.1 [Remember me] ボックスをテストする
テスト内でユーザーがログインできるようにする。
log_in_as
というヘルパーメソッドを作成。
def log_in_as(user)
session[:user_id] = user.id
end
統合テストでも同様のヘルパーを実装。
class ActionDispatch::IntegrationTest
# テストユーザーとしてログインする
def log_in_as(user, password: 'password', remember_me: '1')
post login_path, params: { session: { email: user.email,
password: password,
remember_me: remember_me } }
end
end
cookies[:remember_token]
これだと常にnil
となるので、文字列をキーに返すようにする。
cookies['remember_token']
###9.3.2 [Remember me] をテストする
current_user
内のある複雑な分岐処理については、テストが行われていない。current_user
をリファクタリングするのであれば同時にテストも作成しておくことが重要らしい。
以下を作る。
$ touch test/helpers/sessions_helper_test.rb
テスト手順は
・fixtureでuser
変数を定義する
・渡されたユーザーをremember
メソッドで記憶する
・current_user
が、渡されたユーザーと同じであることを確認する
永続的セッションのテスト
require 'test_helper'
class SessionsHelperTest < ActionView::TestCase
def setup
@user = users(:michael)
remember(@user)
end
test "current_user returns right user when session is nil" do
assert_equal @user, current_user
assert is_logged_in?
end
test "current_user returns nil when remember digest is wrong" do
@user.update_attribute(:remember_digest, User.digest(User.new_token))
assert_nil current_user
end
end
assert_equal
の引数は「期待する値, 実際の値」の順序で書く。
##感想
今回はログイン機能を拡張しました。やっぱり章が進むほど、何言ってるかわかんなくなってくるので、要勉強だなとつくづく思います。。
##疑問・単語集
*トークン パスワードの平文と同じような秘密情報
パスワードとトークンとの一般的な違いは、パスワードはユーザーが作成・管理する情報であるのに対し、トークンはコンピューターが作成・管理する情報である点