【自分用】rails チュートリアル 8章

やること：　ログイン機能の実装
ログインしている状態を保持し、また破棄する（認証システム Authentification System）
ログイン済みの場合のみ接続できるページや扱える機能を制御（認可モデル　Authorization Model）
ブラウザを閉じるとログインを破棄する（session）
ユーザーのログイン情報を自動で保存する（cookie）
ユーザーがチェックボックスをオンにした場合のみログインを保存する（remember me）

8.1 セッション
HTTPはステートレス（Stateless:状態がない）なプロトコル
リクエストが終わると全て忘れてしまう
故にこのプロトコルは頑丈

ブラウザのあるページから別のページに移動するときにユーザーのIDを保持しておく方法がHTTPプロトコルの中にはない
HTTPより上の階層にある、半永続的なコンピュータ間の接続をセッションという
セッションに別途接続を設定する

railsでセッションを実装する方法として一般的なのはcookiesを使うこと
cookiesとはブラウザに保存される小さなテキストデータであり、別のページに移動しても破棄されないのでここにユーザーIDなどの情報を保存できる
アプリケーションはcookies内のデータを使って、ログイン中のユーザーが所有する情報をデータベースから取り出せる

8.2まではRailsのsessionというメソッドを使い一時セッションを作成
これはブラウザを閉じると自動で終了する

セッションをRESTfulなリソースとしてモデリングできると、他のRESTfulリソースと統一的に理解できて便利
ログインページでnewで新しいセッションを出力し、そのページでログインするとcreateでセッションを実際に作成して保存し、ログアウトするとdestroyでセッションを破棄する
Usersリソースと違うのはUsersリソースではバックエンドでUserモデルを介してDB上の永続的データにアクセスするが、sessionリソースでは代わりにcookiesを保存場所として使う
ログインの仕組みの大半はcookiesを使った認証メカニズムによって構築されている

まずはSessionコントローラ、ログイン用のフォーム、両者に関連するコントローラのアクションを作成
8.2ではセッション操作に必要なコードを追加してログインを完成させる

トピックブランチに移動
$ git switch -c basic-login

8.1.1　Sessionsコントローラ
ログインとログアウトの要素をSessionsコントローラの特定のRESTアクションに対応させる
ログインのフォームはnewアクション、createアクションにPOSTリクエストを送るとログインする。destroyアクションにdeleteリクエストでログアウト

リスト8.1 Sessionsコントローラとnewアクション生成
（createやdestroyには対応するビューが必要ないので、無駄なビューを作成しないためにnewだけを指定）

リスト8.2: リソースを追加してRESTfulアクションをgetできるように
Usersリソースには専用のresourcesメソッドを使ってRESTfulなルーティングを自動的にフルセットで利用できるようにした
Sessionリソースではフルセットはいらないので名前付きルーティングだけを使う。GETリクエストやPOSTリクエストをloginルーティングで、DELETEリクエストをlogoutルーティングで扱う

リスト 8.3:Sessionsコントローラのテストで名前付きルートを使うようにする
    リスト8.1を更新して新しいログイン用の名前付きルートを使えるように

!rails routes コマンドでルーティングを確認できる

演習１ 受け取るアクションと変更するアクション
　　　　　　　２　　 rails routes | grep sessions


8.1.2 ログインフォーム
ログインフォームを作成。ログインフォームとユーザー登録フォームの違いは４つあったフィールドがemailとpasswordの２つに減っている
ログインフォームで入力した情報に誤りがあったときはログインページをもう一度表示してエラーメッセージを表示する
7.3.3ではエラーメッセージの表示に専用のパーシャルを使ったがそのパーシャルではActiveRecordによって自動生成されるメッセージを使っていたが、今回はActiveRecordオブジェクトではないので以前のように自動でエラ〜メッセージを表示してくれない
だからフラッシュメッセージでエラーを表示する

以前リスト7.15の時はform_withヘルパーでユーザーのインスタンス変数@userを引数で受け取った
<%= form_with(model: @user) do |f| %>

セッションフォームとユーザー登録フォームの主な違いはセッションにはsessionモデルがなく、そのため@sessionのようなインスタンス変数に相当するものもない点
したがって新しいセッションフォームを作成するときにform_withヘルパーに渡す情報は異なる

form_with(model: @user)
railsではこのように書くだけで フォームのactionは/usersというurlへのpostであると自動的に判定する
セッションの場合はリソースのスコープ（ここではセッション）とそれに対応するURLを具体的に指定する
form_with(url: login_path, scope: session)

リスト8.4 ログインフォームのコード

演習1. <form accept-charset="UTF-8" action="/login" method="post">とあるので、POST	/login	login_path	create	新しいセッションの作成（ログイン）に到達する。

8.1.3 ユーザーの検索と認証
ログインでセッションを作るとき最初に行うのは入力が無効な場合の処理
最初にフォームが送信された時の動作を理解
次にログインが失敗した場合に表示されるエラーメッセージを配置する
次にログインに成功した場合に使う土台部分を作成。ここではログインが送信されるたびにパスワードとメールアドレスの組み合わせが有効かどうかを判定する

まず最小限のcreateアクションをsessionsコントローラで定義し、空のnewアクションとdestroyアクションもついでに作成しておく
createアクションの中では何も行われないが、アクションを実行するとnewビューがレンダリングされるので十分
リスト8.6

paramsハッシュに含まれている送信結果には次のようにsessionキーの下にメールアドレスとパスワードがある
ネストしたハッシュになっている
{ session: { password: "foobar", email: "user@example.com" } }
上の意味は、次のようなハッシュがあり
params[:session]
このハッシュの中にまたハッシュがあり
{ password: "foobar", email: "user@example.com" }
次のようにデータにアクセスできる
params[:session][:email]
createアクションの中でユーザー認証に必要なあらゆる情報をparamsハッシュから簡単に取り出せる

User.find_byメソッドとauthenticateメソッドで必要な情報を取り出せる
リスト8.7 ユーザーをDBから見つけて検証する
user = User.find_by(email: params[:session][:email].downcase)
送信されたメールアドレスを使ってDBからユーザーを取り出している
user && user.authenticate(params[:session][:password])
論理積（and）である&&は取得したユーザーが有効かどうかを決定するために使う
ユーザーがDBにあり、かつ、認証に成功した場合にのみtrueになる

8.1.4 フラッシュメッセージを表示する
セッションではActive Recordのモデルを使っていないため、フラッシュメッセージを表示する
flash[:danger]で表示させられるが、このままだと消えずに表示され続ける
リスト8.8 ログイン失敗時の処理（誤りあり）
どこが誤りかというと、一度表示されると消えずに残ってしまう。
表示したテンプレートをrenderメソッドで強制的に再レンダリングしてもリクエストと見なされないためリクエストのメッセージが消えない

8.1.5 フラッシュのテスト
エラーをキャッチするテストを先に書いて、そのエラーが解決するようにコードを書く
最初に統合テストを生成
$ rails generate integration_test users_login

図 8.6と図 8.7の手順をテストコードで再現する
流れ
ログイン用のパスを開く
新しいセッションのフォームが正しく表示されたことを確認する
わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
新しいセッションのフォームのフォームが正しいステータスを返し再度表示されることを確認する
フラッシュメッセージが表示されることを確認する
別のページ（homeページなど）に一旦移動する
移動先のページでフラッシュページが表示されていないことを確認する
リスト8.9 上の流れをテストに実装
現時点ではred

解決するにはflash[:danger]をflash.now[:danger]に書き換え
flash.nowのメッセージはその後リクエストが発生したときに消滅する
リスト8.11  ログイン失敗時の正しい処理

8.2 ログイン
ログイン中の状態で有効な値の送信をフォームで正しく扱えるようにする
cookiesを使った一時セッションでユーザーをログインできるようにする

複数のコントローラからログイン関連のメソッドを呼び出せるようにする
各ヘルパーはコントローラを生成したときに自動的に用意されて、どこから呼び出せるようにするかを決める
全コントローラの親クラスのApplicationコントローラにセッション用のヘルパーを読み込ませ、どのコントローラからでもログイン関連のメソッドを呼び出せるようにする
リスト8.1.3 ApplicationコントローラにSessionヘルパーモジュールを読み込む

8.2.1 log_inメソッド
sessionメソッドで単純なログインをする(8.1.1のSessionsコントローラとは無関係)
このsessionメソッドはハッシュのように扱えるので次のように代入する
session[:user_id] = user.id

↑ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される
この後のページでsession[:user_id]を使ってユーザーIDを元通りに取り出せる

一方、cookiesメソッドとは対照的に、sessionメソッドで作成された一時cookiesはブラウザを閉じた瞬間に終了する

リスト8.14 同じログイン手法を使いまわせるように、Sessionsヘルパーにlog_inという名前のメソッドを定義する

sessionメソッドで作成された一時cookiesは自動的に暗号化されているので、コードは保護される
攻撃者がcookiesから情報を盗んでも、本物のユーザーとしてログインできない
ただし、セッションはセッションハイジャック攻撃に対して脆弱
セッションハイジャックとは、攻撃者があるユーザーのセッションidのコピーを手に入れて、そのユーザーとしてログインする攻撃方法
現時点でこのセッションはブラウザを閉じると自動的に消滅するので懸念は小さい

セッション固定（攻撃者が既に持っているセッションidをユーザーに使わせるように仕向けることで、攻撃者がユーザーとセッションを共有するもの）への対策は、ユーザーがログインする直前にセッションを必ず即座にリセットすること
これにより、攻撃者がユーザーを騙して使わせようとしているセッションidがクリアされ、新しいidがセッションハッシュで使われるようになる。reset_sessionメソッドで行える

リセット8.15 セッションのcreateアクション完成
redirect_to user と user_url(user)は同じ

これでログインフォームが正常に動作するようになった

8.2.2 現在のユーザー
ユーザーIDを一時セッションの中に安全に置けるようになったので、一時セッションのユーザーIDを別のページで取り出せるようにする
そのためにcurrent_userメソッドを定義してセッションIDに対応するユーザー名をDBから取り出せるようにする
次のようなコードを書けるようにしたい
<%= current_user.name =>
次のようなコードでユーザーのプロフページに簡単にリダイレクトできる
redirect_to current_user

現在のユーザーを検索する方法として、findメソッドを使う
User.find(session[:user_id])
しかし、ユーザーIDが存在しない場合にfindを使うと例外が発生してしまう
プロフページでは適切だった（IDが無効な場合は例外を発生してくれなければ困るから）
しかし、ユーザーがログインしていないなどの状況が考えられる今回のケースではsession[:user_id]の値がnilになる可能性がある
これを修正をするためにcreateメソッド内でメールアドレスの検索に使ったのと同じfind_byメソッドを使う
今回はidで検索する
User.find_by(id: session[:user_id])
これでIDが無効（ユーザーが存在しない）な場合にも例外が発生せず、nilを返す
↑を使いcurrent_userを定義
def current_user
  if session[:user_id]
    User.find_by(id: session[:user_id])
  end
end

nilを返してほしい理由
current_userメソッドが1リクエスト内の処理で何度も呼び出されてしまうとその回数だけDBへの問い合わせが発生して処理に時間がかかってしまう

Rubyの慣習に従ってUser.find_byの実行結果をインスタンス変数に保存する工夫もしている
こうするとDBへの問い合わせが最初の１回だけで済む
このようなメソッド呼び出しの結果を変数に保存し、次回以降の呼び出しで再利用することをメモ化(memoization)という
if @current_user.nil?
  @current_user  = User.find_by(id :session[:user_id])
else
  @current_user
end
or演算子||を使うと↑のコードが１行で書ける
@current_user = @current_user || User.find_by(id: session[:user_id])
@current_userに何も代入されていないときだけfind_by呼び出される
短縮形で書くのがベター
@current_user ||= User.find_by(id: session[:user_id])

コラム8.1 ||=について
Rubyで広く使われている
rubyでは「変数の値がnilなら変数に代入するが、nilでなければ代入しない（変数の値を変えない）」　という操作がよく使われる
>> @foo
=> nil
>> @foo = @foo || "bar"
=> "bar"
>> @foo = @foo || "baz"
=> "bar"
||を左から順に評価し、演算子の左の値が最初にtrueになった時点で処理を終了するのを短絡評価(short-circuit evaluation)という

リスト8.16 セッションに含まれる現在のユーザーを検索する
current_userメソッドが動作するようになってユーザーがログインしているかどうかに応じてアプリの動作を変更できるようになった

8.2.3 レイアウトリンクを変更する
ログインしている時とそうでない時のレイアウトを変更
　ログアウトリンク、ユーザー設定リンク、ユーザー一覧リンク、プロフ表示リンクなど追加
レイアウトのリンクを変更するにはERBコードの中でif-else文を使用し条件に応じて表示するリンクを使い分ける
<% if logged_in? %>
  ログインユーザー用のリンク
<% else %>
  ログインしていないユーザー用のリンク
<% end %>

必要な論理値を返すlogged_in?メソッドを作る
ログインしている＝sessionにユーザーIDが存在している＝current_userがnilではない
リスト8.18 logged_in?メソッド作成
否定演算子「!」を使う

ログアウト用リンクでは、リスト8.2のログアウト用パスを使う
<%= link_to "Log out", logout_path, data: { "turbo-method": :delete } %>
ログアウト用リンクの引数としてハッシュを渡している
このハッシュはHTTPのDELETEリクエストを使うよう指示している

プロフ用リンクではリスト8.16のcurrent_userメソッドを使う
<%= link_to "Profile", current_user %>
↑↓同じ意味
<%= link_to "Profile", user_path(current_user) %>
リスト8.19 ログイン中のレイアウト変更 リンク変更

これでログインしたユーザーのリンクが表示される。ブラウザを終了するとアプリがユーザーのログイン状態を消去することも確認できる

8.2.4 メニューのトグル機能
ページ内にあるドロップダウンメニューを検索し、メニューの表示と非表示をJavaScriptでトグルできるようにする(ブラウザでメニューリンクをクリックするとメニュー表示が開いたり閉じたりする機能)
そのためにjavascriptを導入する
importmapと呼ばれる手法で標準化される
railsではimportmap-railsというgemを用いて導入する

リスト8.２０ importmapとTurboとStimulusをインストール
TurboとStimulusは、コラム1.1のHotwireフレームワークの一部
通常はrails newでアプリを作成したときに自動的に実行されるので通常は行う必要はない
-skip-bundleオプションでrails new実行したので↑のコードを手動で実行する

↑のコードを実行するとjavascriptのマニフェストファイルやアプリのレイアウトファイルなどが自動的に更新される
リスト8.21 マニフェストファイル
リスト8.22 レイアウトファイル

この設定ができるとドロップダウンメニューを有効にする準備が整う
1.CSSのactiveクラスの振る舞いを定義してメニューが表示されるようにする リスト8.23
2.javascriptを置くためのcustomディレクトリとmenu.jsファイルを新たに作成する リスト8.24
3.menu.jsファイルにリスト8.25のコードを書く
4.importmapを使ってcustomディレクトリが存在することをrailsに認識させる
5.menu.jsをapplication.jsにインポートする

リスト8.23 ドロップダウンメニューを有効にする
手順１で、blockのdisplay設定を変更するactiveクラスを追加
これによりbootstrapのdropdown-toggleクラスの挙動がオーバーライドされドロップダウンメニューが表示される

リスト8.24 ドロップダウンメニューで使うカスタムjavascriptファイル追加
手順2で、app/javascriptディレクトリの下にcustomディレクトリを作成し、そこにmenu.jsファイル作成

リスト8.25 アカウントメニューをトグルするjavascriptコード
手順３で、menu.jsファイルにjavascriptのイベントリスナーを追加してアカウントメニューがクリックされるまで待機し、クリックされるたびにリスト8.23のactiveクラスを変更して表示と非表示を切り替える
ブラウザでのページの読み込みが完了してからイベントリスナーが追加されるようにするため、ページ読み込みイベント自身にもイベントリスナーを追加
preventDefault() デフォルトのリンクの振る舞いを止める
stopPropagation() リンク先への伝播を止める

リスト8.26 customディレクトリにあるjavascriptコードをimportmapの設定に追加
手順4で、importmapを使ってプロジェクトのカスタムjavascriptコードがapp/javascript/customにあることをrailsに認識させる

リスト8.27 ドロップダウンメニューにjavascriptをインポート
手順５で、ドロップダウンメニューのJavaScriptコードをインポートする行を追加

railsサーバーを再起動して変更を反映する

8.2.5 モバイル向けスタイリング
画面デザインをモバイルデバイス向けに調整する

リスト8.28 ビューポート用のmetaタグ追加
ビューポートを使うと開発者がPCモードとモバイルモードを切り替えられるようになる
最新のrailsには自動的に追加されているはず

リスト8.29 ハンバーガーメニュー追加
ヘッダーファイルにハンバーガーメニュー用のマークアップ追加

次にjavascriptでリスト8.25と同等のハンバーガーメニュー切り替え機能を追加する
違いはcssの”active”クラスをトグルするのではなく、bootstrapの"collapse"クラスをトグルすること

リスト8.30 モバイルメニューをトグルする
ハンバーガーメニューのCSS ID(リスト8.29の"hamburger")に対応するイベントリスナーをリスト8.25に追加する
対応する要素(リスト8.29の"navbar-menu")をトグルする

最後にフッターにcssを追加する
リスト8.31 pc用のcssとモバイル用のcssをメディアクエリで切り替える

モバイル向けのスタイル適用が完了したので、レイアウトリンクのテストを追加していく

8.2.6 レイアウトの変更をテストする

統合テストを書いてログイン成功のテスト作成
1.ログイン用のパスを開く
2.セッション用パスに有効な情報をpostする
3.ログイン用リンクが表示されなくなったことを確認する
4.ログアウト用リンクが表示されていることを確認する
5.プロフィール用リンクが表示されていることを確認する

↑の変更を確認するためには、テスト時に登録済みユーザーとしてログインしておく必要がある
テスト時にログインするためのユーザーデータをfixtureで作成
fixtureを使ってテストに必要なデータをtestDBに読み込んでおける
6.25ではメールの一意性のテストがパスするためにデフォルトのfixtureを削除していたが、今度は自分で空のfixtureファイルを作成する

現時点のテストではユーザーが1人いれば十分なので、そのユーザーに有効な名前とメアドを設定しておく
テスト中に自動ログインするために有効なパスワードも用意してsessionsコントローラのcreateアクションに送信されたパスワードと比較できるようにする必要がある
図の6.9のデータモデルを見るとpassword_digest属性をユーザーのfixtureに追加すればいいと分かる。そのためにdigestメソッドを定義する

has_secure_passwordでbcryptパスワードが作成されるので同じようにfixture用のパスワードを作成
BCrypt::Password.create(string, cost: cost)
stringはハッシュ化する文字列、costはコストパラメータと呼ばれる値。コストパラメータでは、ハッシュを算出するための計算コストを指定する。値を高くすればハッシュからオリジナルのパスワードを推測することが困難になる。
テスト中はコストを高くする必要はないので、digestメソッドの計算はなるべく軽くする。
secure_passwordのソースコードにある↓の行を参考
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                              BCrypt::Engine.cost

digestメソッドはさまざまな場面で利用するのでUserモデルのuser.rbに置いておく
この計算はユーザーごとに行う必要はないので、fixtureファイルなどにわざわざユーザーオブジェクトにアクセスする必然性はないのでインスタンスメソッドで定義する必要はない
digestメソッドをUserクラス自身に配置してクラスメソッドにする
リスト8.33 fixture向けのdigestメソッド追加

digestメソッドができたので有効なユーザーを表すfixtureを作成できるようになった
fixtureではインデントに使うスペースの数が重要。タブを使わない
リスト8.34 ユーザーログインのテストで使うfixture
fixtureにはERBが書ける。↑のERBではテストユーザー用の有効なパスワードを作成している

has_secure_passwordで必要になるpassword_digest属性は準備できたが、ハッシュ化されていない生のパスワードも参照できると便利
しかしfixtureではそれができない。
password属性を追加しても、そのようなカラムはDBに存在しないとエラーが発生する
よってテスト用のfixtureでは全て同じパスワード「password」を使う

テストでは次のようにfixtureのデータを参照できるようになる
user = users(:michael)
usersはfixtureのファイル名users.ymlを指し、:michaelシンボルはユーザーを参照するためのキー

レイアウトのリンクをテストできる状態になった
リスト8.35 有効な情報でユーザーがログインできるかテスト
assert_redirected_to @user リダイレクト先が正しいか確認
follow_redirect!　実際に移動
assert_select "a[href=?]", login_path, count: 0 ログイン後にログイン用リンクが表示されないことを確認

8.2.7 ユーザー登録時にログインする
認証システムが動作するようになったが、登録完了したユーザーがデフォルトでログインしていない
ユーザー登録中にログインも済ませることにする
Userコントローラのcreateアクションにlog_inを追加するだけ
セッション固定攻撃から保護するためにリスト8.15と同様にログイン直前にreset_sessionメソッド呼び出しも追加する
リスト8.40 ユーザー登録中にログインする

↑の動作をテストするためリスト7.31のテストに1行追加してユーザーがログイン中かどうか確認
is_logged_in?ヘルパーメソッド　テストのセッションにユーザーがあればtrue、それ以外はfalse
ヘルパーメソッドはテストから呼び出せないのでリスト8.18のようにcurrent_userを呼び出せない
sessionメソッドはテストでも利用できるのでこれを使う
logged_in?の代わりにis_logged_in?を使ってヘルパーメソッド名がテストヘルパーとsessionヘルパーと同じにならないようにする
リスト8.41 テスト中のログインステータスを論理値で返すis_logged_in?メソッド
リスト8.42 ユーザー登録後のログインテスト

8.3 ログアウト
ログアウト機能を実装
ユーザーセッションを破棄するための有効なアクションをコントローラで作成するだけ

sessionsコントローラのアクションはrestfulルールに従っていた
newでログインページを表示し、createでログインを完了するといった具合
セッション破棄するdestroyアクションも同じ要領で行う

ログアウトの処理はリスト8.14のlog_inメソッドの実行結果を取り消す
セッションのdeleteメソッドでuser_idを削除する
session.delete(:user_id)

ログアウト時にreset_sessionメソッドで全てのセッション変数を確実にリセットする
安全のために@current_userもnilに設定する
リスト8.44 log_outメソッド
↑のメソッドはsessionsコントローラのdestroyアクションで使える

リスト8.45 セッション破棄 ユーザーのログアウト
status: :see_otherというhttpステータスも指定している
railsでturboを使うときは、このように303 see other ステータスを指定することでdeleteリクエスト後のリダイレクトが正しく振る舞うようにする(リスト7.18の:unprocessable_entityと見比べる)

ログアウト機能テストするためにリスト8.35のユーザーログインテストに手順を追加する
ログイン後、deleteメソッドでDELETEリクエストをログアウト用パスに発行し、ユーザーがログアウトしたらステータスコード:see otherによってルートURLにリダイレクトされることを確認。ログイン用リンクが再度表示されることとyログアウト用リンクとプロフィール用リンクが非表示になることも確認

リスト8.46 ログアウトテスト
テストでis_logged_in?ヘルパーメソッドを利用できるようにしてあったおかげで有効な情報をセッション用パスにPOSTした直後にassert_is_logged_in?で簡単にテストできた

8.4 最後に
次の章ではログイン機能を改善してセッションより長くログイン情報維持する方法を見ていく

$ rails test
$ git add -A
$ git commit -m "Implement basic login"
$ git switch main
$ git merge basic-login

$ rails test
$ git push

8.4.1 本章のまとめ
・railsのsessionメソッドを使うと、あるページから　別のページに移動する時の状態を保持できる。
一時的な状態の保存にはcookiesも使える
・ログインフォームでは、ユーザーがログインするための新しいセッションが作成できる
・flash.nowメソッドを使うと、描画済みのページにもフラッシュメッセージを表示できる
・テスト駆動開発は回帰バグを防ぐときに便利
・sessionメソッドを使うと、ユーザーIDなどをブラウザに一時的に保存できる
・ログインの状態に応じて、ページ内で表示するリンクを切り替えることができる
・統合テストでは、ルーティング、DBの更新、レイアウトの変更が正しく行われているかを確認できる