エンジニア志望のブログ

Railsチュートリアル第9章

はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境  Ruby: 2.7.2 , Rails: 6.1.4

メモ

Remember me機能

ユーザーのログイン状態を、ブラウザを閉じたあとでも有効にする機能
sessionメソッドを利用したブラウザへのセッション情報は安全が保たれるが、ブラウザを閉じるセッション情報は消えてしまう
そこで、セッションの永続化をcookiesメソッドを利用し、安全性を加味しながら実現する

セッション

一時セッション :ブラウザを閉じると消えるセッションの事(sessionメソッドを利用)
永続的セッション:ブラウザを閉じても半永久的に消えないセッション(cookiesメソッドを利用)

cookieメソッドでの永続化によるデメリット

cookiesメソッドでは永続的にログインした状態が保たれるが、セッションハイジャック攻撃を受ける可能性がある。

セッションハイジャックの主な手法

  1. 管理の甘いネットワークを通過するネットワークパケットからパケットスニッファというソフトでcookieを直接取り出す
  2. データベースから記憶トークンを取り出す
  3. クロスサイトスクリプティングを使う
  4. ユーザがログインしているパソコンやスマホを直接操作してアクセス権を奪い取る
セッションの永続化

セッションの永続化を実現するためには

永続セッション実装の流れ
 記憶トークンをデータベースに保存するためにUserテーブルにremember_digestカラムを追加
 記憶トークンを生成する(ランダムな文字列を生成しこれを記憶トークンとして利用)
 ユーザーオブジェクトに記憶トークンを記憶させる
 ハッシュ化した記憶トークンをデータベース(Userテーブルのremember_digestカラム)に登録
 ユーザーオブジェクトの記憶トークンとユーザーIDをcookiesメソッドを利用してブラウザに保存

トーク

パスワードと同じような秘密情報

パスワードとトークンの一般的な違い
 ・パスワード:ユーザが作成、管理する情報
 ・トークン :コンピュータが作成、管理する情報

記憶トーク

ランダムな文字列を利用

SecureRandom.urlsafe_base64

ランダムな22文字の文字列を生成する

cookiesメソッドで状態を保持

sessionのようにハッシュとして扱う
cookiesは1つのvalueとオプションのexpires(有効期限)を持つ

記憶トークンをブラウザに記憶
cookies[:remember_token] = { value:   remember_token,
                             expires: 20.years.from_now.utc }

valueに記憶トークンをexpiresに有効期限を渡す。
次のコードも同じ処理をする

cookies.permanent[:remember_token] = remember_token

有効期限を20年とする設定がよく使われるため上記のようなpermanentというメソッドが追加された。

ユーザーIDをブラウザに記憶

signedメソッドで暗号化(署名付きcookie)、permanentメソッドで20年保持

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

cookiesから暗号化されたユーザーIDを取り出す

cookies.signed[:user_id]

cookies.signed[:user_id]では自動的にcookiesの暗号化を解除し、
続いてbcryptを使ってcookies[:remember_token]がremember_digestと一致するかの確認をする

永続的セッションの削除

cookiesメソッドでブラウザのcookie情報を削除する

cookies.delete(:user_id) #ユーザーIDを削除
cookies.delete(:remember_token) #記憶トークンを削除

assignsメソッド

コントローラーのアクションが実行された後に、コントローラーで定義されたインスタンス変数を参照する。

9.3.1の演習1ではlogin_pathにPOSTリクエストを送りsession_controllerのcreateアクションが実行されたため、このcreateアクションで定義されたインスタンスメソッドを参照している。

raiseメソッド

プログラムの中で意図的に例外を発生させる

herokuメンテナンスモード

メンテナンスモードをオンにすることでページにアクセスできない状態にする。

メンテナンスモードをオン

$ heroku maintenance:on

メンテナンスモードをオフ

$ heroku maintenance:off

演習

9.1.1

演習1

コンソールを開き、データベースにある最初のユーザーを変数userに代入してください。その後、そのuserオブジェクトからrememberメソッドがうまく動くかどうか確認してみましょう。また、remember_tokenとremember_digestの違いも確認してみてください。

remember_digestにはremember_tokenをハッシュ化し値が保存されている。

irb(main):001:0> user = User.first
   (0.4ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
irb(main):002:0> user.remember
  User Update (0.3ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2021-07-11 08:36:08.652655"], ["remember_digest", "$2a$12$1VR./gDUj1Tt8XBN/SA3UO.TyXAKajeuN1868HOW7U1GsULWC9rX2"], ["id", 1]]
  TRANSACTION (0.6ms)  commit transaction
=> true
irb(main):003:0> user.remember_token
=> "AHgb7ct6KjACo4zSQ2ye2A"
irb(main):004:0> user.remember_digest
=> "$2a$12$1VR./gDUj1Tt8XBN/SA3UO.TyXAKajeuN1868HOW7U1GsULWC9rX2"

9.1.2

演習2

コンソールを開き、リスト 9.6のauthenticated?メソッドがうまく動くかどうか確かめてみましょう。

irb(main):001:0> user = User.first
   (0.5ms)  SELECT sqlite_version(*)
  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....
irb(main):002:0> user.remember
  User Update (0.3ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2021-07-11 09:13:30.028775"], ["remember_digest", "$2a$12$YPaIMQRoOJV/a0Qp08e9Lu431JW2QKMu787PgwLVDfid8pHxfiy.a"], ["id", 1]]
  TRANSACTION (0.6ms)  commit transaction
=> true
irb(main):003:0> user.authenticated?(user.remember_token)
=> true

9.3.1

演習1

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

createアクション

  def create
    @user = User.find_by(email: params[:session][:email].downcase)
    if @user&.authenticate(params[:session][:password])
      log_in @user
      params[:session][:remember_me] == '1'? remember(@user) : forget(@user)
      redirect_to user_url(@user)
    else
      flash.now[:danger] = 'Invalid email/password combination' 
      render 'new'
    end
  end

統合テスト

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

おわりに

9章は内容が一気に難しくなった気がします。
処理が複雑になり、どう動いているか意識しながら進めないと混乱しますね、、