(ギリ)20代の地方公務員がRailsチュートリアルに取り組みます【第9章】


前提

・Railsチュートリアルは第4版
・今回の学習は3周目(9章以降は2周目)
・著者はProgate一通りやったぐらいの初学者

基本方針

・読んだら分かることは端折る。
・意味がわからない用語は調べてまとめる(記事最下段・用語集)。
・理解できない内容を掘り下げる。
・演習はすべて取り組む。
・コードコピペは極力しない。

 
 続いて第9章、認証システムの開発・第4段回目、ログイン実装後半です。第8章では一時的なセッションだったものを、cookieを利用して永続的なものに切り替えていきます。
 セキュリティに関する用語が飛び交いますが、ある程度内容は押さえていきましょう。ドコモ口座の件もありますし、セキュリティの意識を高めねば。
 
本日のBGMはこちら。
死んだ僕の彼女 "Aki No Hachiouji"
徐々に秋の訪れを感じますね。

 

【9.1.1 記憶トークンと暗号化 メモと演習】

 パスワード:ユーザーが作成・管理
 トークン :コンピュータが作成・管理
 urlsafe_base64:Ruby標準ライブラリのSecureRandomモジュールにあるメソッド。A–Z、a–z、0–9、"-"、"_"のいずれかの文字 (64種類) からなる長さ22のランダムな文字列を返す。

 下記の5つの永続的セッション作成方針を頭に入れて進めましょう。
1.記憶トークンにはランダムな文字列を生成して用いる。
2.ブラウザのcookiesにトークンを保存するときには、有効期限を設定する。
3.トークンはハッシュ値に変換してからデータベースに保存する。
4.ブラウザのcookiesに保存するユーザーIDは暗号化しておく。
5.永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。

1. コンソールを開き、データベースにある最初のユーザーを変数userに代入してください。その後、そのuserオブジェクトからrememberメソッドがうまく動くかどうか確認してみましょう。また、remember_tokenとremember_digestの違いも確認してみてください。
→ 下記。ハッシュ化された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: "Rails Tutorial", email: "[email protected]", created_at: "2020-09-12 09:09:50", updated_at: "2020-09-12 09:09:50", password_digest: "$2a$10$hrOEzw0faSd4yurmH8bQJOnggeNnUqTZg33yE9g7Tnk...", remember_digest: nil>
>> user.remember
   (0.1ms)  begin transaction
  SQL (3.0ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2020-09-13 22:33:06.439353"], ["remember_digest", "$2a$10$IQ/x1avxRSAG281J18FRi.f2icjx8Kac5y8bWua5IDVae.C.Kdwcu"], ["id", 1]]
   (5.9ms)  commit transaction
=> true
>> user.remember_token
=> "aGtKYk5iEjSHFs16uB7xTQ"
>> user.remember_digest
=> "$2a$10$IQ/x1avxRSAG281J18FRi.f2icjx8Kac5y8bWua5IDVae.C.Kdwcu"

 
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「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります。
→ 指示通り書くと、両方ともGREENです。ここで出てくる書き方について調べてみました。最初のUser→selfに置き換えたものを特異メソッド方式、class << selfでまとめたものを特異クラス方式と呼ぶようです。詳しくはこちらの記事へ。

 

【9.1.2 ログイン状態の保持 メモと演習】

 permanentメソッド:20年後に期限切れにする。
 signedメソッド:デジタル署名と暗号化の両方の処理を行う。
 
1. ブラウザのcookieを調べ、ログイン後のブラウザではremember_tokenと暗号化されたuser_idがあることを確認してみましょう。
→ たしかに両方とも増えていました!

 
2. コンソールを開き、リスト 9.6のauthenticated?メソッドがうまく動くかどうか確かめてみましょう。
→ 演習1で確認したremember_tokenを引数に入れればOK。

user = User.first
略
user.authenticated?("演習1のremember_token")
=> true

 

【9.1.3 ユーザーを忘れる メモと演習】

 ユーザーを忘れる=remember_digestをnilで更新する。

1. リスト 9.16で修正した行をコメントアウトし、2つのログイン済みのタブによるバグを実際に確かめてみましょう。まず片方のタブでログアウトし、その後、もう1つのタブで再度ログアウトを試してみてください。
→ NoMethodError in SessionsController#destroy
undefined method `forget' for nil:NilClass

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

 
3. 上のコードでコメントアウトした部分を元に戻し、テストスイートが red から greenになることを確認しましょう。
→ Yes, GREEN !

 

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

1. 8.1.4の処理の流れが正しく動いているかどうか、ブラウザで確認してみてください。特に、flashがうまく機能しているかどうか、フラッシュメッセージの表示後に違うページに移動することを忘れないでください。
→ 試してみましょう。違うページに行くとフラッシュが消えます。

 

【9.2. [Remember me]チェックボックス メモと演習】

 三項演算子というものが出てきました。if-else文を一行で書けるようです。これでリスト8.21のコードの形がみえてきました(いまだに::はしっくりきてないけど)。つまり、下のコードは

cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost

このようになると。

if cost = ActiveModel::SecurePassword.min_cost
  BCrypt::Engine::MIN_COST
else
  BCrypt::Engine.cost
end

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

 
2. コンソールを開き、三項演算子を使った実例を考えてみてください (コラム 9.2)。
→ 至極シンプルに下記

>> x = 6
=> 6
>> x % 3 == 0 ? "3の倍数" : "ちゃうな"
=> "3の倍数"

 

【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] チェックボックスのテストを改良してみてください。
→ userを@userに、ログインテスト該当箇所を下記のとおりに。

sessions_controller.rb
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
users_login_test.rb
  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_equal cookies['remember_token'], assigns(:user).remember_token
  end

このログインテストのcookies['remember_token']なんですが、当初はcookies[:remember_token]を試したところテスト通過。なぜだろうと思って調べると、こちらの記事が。現在はシンボルでもOKなのかな?でもチュートリアルのRailsのバージョンは古いしなあ…。

 

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

1. リスト 9.33にあるauthenticated?の式を削除すると、リスト 9.31の2つ目のテストで失敗することを確かめてみましょう (このテストが正しい対象をテストしていることを確認してみましょう)。
→ 該当箇所で失敗してました。

 

第9章まとめ

・cookiesメソッドでユーザーidと記憶トークンを永続化。
・データベースに絡む動作はUserクラスに定義。Sessionコントローラで使用するメソッドは主にSessionヘルパーに定義。前者は後者でも使用。
・remember_me機能は値が1か0かでクッキーを保持するか削除するか分けるスイッチのようなもの。
・current_userはセッションかクッキーの状態・情報によって決まる。
・情報技術用語が飛び交っていたので用語集にまとめています。

 
 この章はややこしいですね…。webページにみえてこない部分なので、どうしてもイメージが描き切れない部分があります。とはいえ、セキュリティに関する部分は疎かにできません。時間がたってからもう一回やってもいいかも。

 さて次!第10章!未実装のユーザー機能を実装していきます!

 
⇨ 第10章へ!
⇦ 第8章はこちら
学習にあたっての前提・著者ステータスはこちら
 

なんとなくイメージを掴む用語集

・セッションハイジャック
 通信の当事者でない第三者(攻撃者)が何らかの手段でセッションIDを知ることにより、セッションを乗っ取る攻撃手法。

・パケットスニッファ
 LANアナライザの俗語。LAN上を通過するトラフィックを監視したり記録するためのハードウェアやソフトウェアのこと。

・クロスサイトスクリプティング(XSS)
 Webサイトに利用されるアプリケーションの脆弱性もしくはその脆弱性を悪用した攻撃のこと。特にWeb閲覧者側が制作することのできる動的サイト(例:TwitterなどのSNS、掲示板等)に対して、その脆弱性を利用して悪意のある不正なスクリプトを挿入することにより発生するサイバー攻撃。

・デジタル署名
 書面上の手書き署名のセキュリティ特性を模倣するために用いられる公開鍵暗号技術の一種。

・ソルト
 パスワードやパスフレーズなどのデータをハッシュ化する際に、一方向性関数の入力に加えるランダムなデータのこと。本文中訳注にあるように、人には念をの塩ひとつまみで暗号を強化する。

・assert_empty
 obj.emptyはtrueであると主張する。

・assert_nil
 obj.nil?はtrueであると主張する。