らくだ🐫にもできるRailsチュートリアル|8.2

8.2 ログイン

有効な値の送信があった場合にログインできるようにする!
→cookieを使った一時セッションでログイン
→ブラウザを閉じると自動的に期限切れになるようにする

セッションを実装するには、様々なコントローラやビューでおびただしい数のメソッドを定義する必要があります。(中略)Sessionsコントローラ (8.1.1) を生成した時点で既にセッション用ヘルパーモジュールも (密かに) 自動生成されています。さらに、Railsのセッション用ヘルパーはビューにも自動的に読み込まれます。

ApplicationコントローラにSessionヘルパーモジュールを読み込ませてどのコントローラからでも使えるようにする

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  #Sessionヘルパーモジュールの読み込み
  include SessionsHelper
end

8.2.1 log_inメソッド

Railsで元から定義されているsessionメソッドを使って単純なログインを行えるようにする
8.1.1で生成したSessionsコントローラとは無関係なので注意!!

#sessionメソッドはハッシュの様に扱えるメソッド
session[:user_id] = user.id

上のコードを実行すると、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成されます。この後のページで、session[:user_id]を使ってユーザーIDを元通りに取り出すことができます。一方、cookiesメソッド (9.1) とは対照的に、sessionメソッドで作成された一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了します。

Sessionsヘルパーにlog_inという名前のメソッドを定義して、同じログイン手法を使い回せるようにする

module SessionsHelper
 
  # 渡されたユーザーでログインする
  def log_in(user)
    #session[:user_id]にuser.idを代入する
    session[:user_id] = user.id
  end
end

一時cookieと永続的なcookies

sessionメソッドで作成した一時cookiesは自動的に暗号化されるので
cookieから情報を盗み出されても、それを使って本物のユーザーとしてログインすることは出来ない
→一時的なセッションでブラウザを閉じると有効期限が終了する為
永続的なcookieに関してはセッションハイジャックという攻撃を受ける可能性が常につきまとう!
→9章に続く

createアクションの完了

作成したヘルパーメソッドを使ってユーザーとしてログインできる様に
sessionsコントローラーのcreateアクションを完成させる

・
・
・
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      #log_inメソッド(session[:user_id]にuser.idを代入)
      log_in user
      #詳細画面にリダイレクト
      redirect_to user
    else
      flash.now[:danger] = t('.login_error')
      render 'new'
    end
  end
・
・
・

ログインフォームが作動するようになっている!

演習

  1. 有効なユーザーで実際にログインし、ブラウザからcookiesの情報を調べてみてください。このとき、sessionの値はどうなっているでしょうか? ヒント: ブラウザでcookiesを調べる方法が分からない? 今こそググってみるときです!
  2. 先ほどの演習課題と同様に、Expiresの値について調べてみてください。

1.ブラウザ(chrome)の設定→詳細設定→プライバシーとセキュリティ→サイトの設定
→Cookie とサイトデータ→すべての Cookie とサイトデータを表示→


2.Expires→有効期限(ブラウザ セッションの終了時)

8.2.2 現在のユーザー

ユーザーIDを安全に一時セッションの中に置けるようになった!
なので、そのユーザーIDを別のページで取り出せるようにする
→current_userメソッドを定義する

#ユーザーが存在しない場合、findメソッドでは例外が発生する
User.find(session[:user_id])
#find_byメソッドでは、ユーザーが存在しない場合はnilが返る
User.find_by(id: session[:user_id])

「ユーザーがログインしていない」などの状況が考えられる今回のケースでは、session[:user_id]の値はnilになりえます。

なのでfind_byメソッドを使ってcurrent_userを定義していく

def current_user
  #もしsessionにuser_idが有れば
  if session[:user_id]
    #Userモデルの中のidがsessionに入ったuser_idと合致するデータの1件目を取得
    User.find_by(id: session[:user_id])
  end
  #sessionにユーザーIDが無ければnilが返る
end
#→User.find_byの実行結果をインスタンス変数に保存する
#@current_userがnilであれば
if @current_user.nil?
  #@current_userに代入 Userモデルの中のidがsessionに入ったuser_idと合致するデータ
  @current_user = User.find_by(id: session[:user_id])
#nilでなければ
else
  #@current_user自身を返す
  @current_user
end
 
#演算子を使ってさらにコードを短縮
#@current_userに代入
#@current_user自身もしくはUserモデルの中のidがsessionに入ったuser_idと合致するデータ
@current_user = @current_user || User.find_by(id: session[:user_id])
 
#更に省略形に
@current_user ||= User.find_by(id: session[:user_id])
#上記のコードをヘルパーに追加したもの
・
・
・
  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    #もしsessionにuser_idが有れば
    if session[:user_id]
      #@current_user自身もしくはUserモデルの中のidがsessionに入ったuser_idと合致するデータを@current_userに代入
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
  
end

演習

  1. Railsコンソールを使って、User.find_by(id: …)で対応するユーザーが検索に引っかからなかったとき、nilを返すことを確認してみましょう。
  2. 先ほどと同様に、今度は:user_idキーを持つsessionハッシュを作成してみましょう。リスト 8.17に記したステップに従って、||=演算子がうまく動くことも確認してみましょう。
#1.
>> User.find_by(id: 10)
#対応ユーザーがいないのでnil
=> nil
 
#.2
#sessionに空のハッシュを代入
>> session = {}
#{}が返る
=> {}
#session[:user_id]にnilを代入
>> session[:user_id] = nil
#nilが返る
=> nil
#@current_user自身もしくはUserモデルの中のidがsessionに入ったuser_idと合致するデータを@current_userに代入
>> @current_user ||= User.find_by(id: session[:user_id])
#session[:user_id]がnilなのでnilが返る
=> nil
#session[:user_id]にUserモデルの1つ目のデータのid
を代入
>> session[:user_id]= User.first.id
#1が返る
=> 1
#@current_user自身もしくはUserモデルの中のidがsessionに入ったuser_idと合致するデータを@current_userに代入
>> @current_user ||= User.find_by(id: session[:user_id])
#(@current_userはまだ空なので)id: 1(sessionに入ったuser_id)をUserモデルから取得した内容が返る
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2019-11-11 07:06:24", updated_at: "2019-11-11 07:06:24", password_digest: "$2a$10$IgiS5ygszAZUdJm6JKj.D.eNYlRHOmtjKolGn7W3ZFm...">
#@current_user自身もしくはUserモデルの中のidがsessionに入ったuser_idと合致するデータを@current_userに代入
>> @current_user ||= User.find_by(id: session[:user_id])
#@current_user自身(上で代入されたユーザデータ)が返る
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2019-11-11 07:06:24", updated_at: "2019-11-11 07:06:24", password_digest: "$2a$10$IgiS5ygszAZUdJm6JKj.D.eNYlRHOmtjKolGn7W3ZFm...">

上下(25行目と29行目)の@current_user ||= User.find_by(id: session[:user_id])
の違い

式は条件を満たしたら処理を行って終了する
上の@current_user ||= User.find_by(id: session[:user_id])は
@current_userに何も代入されていないので、次のUser.find_by(id: session[:user_id])に進む
下の@current_user ||= User.find_by(id: session[:user_id])は
@current_userにidが1のユーザーが代入されているので@current_userを返して終了
(Userモデルにデータを探しに行かない)

8.2.3 レイアウトリンクを変更する

ユーザーがログインしている時とそうでない時でレイアウトを変更する
RoRT 本文 8.2.3 図8.7参照
ログインが成功している時はアカウントメニューとして下記を表示

  • 「ログアウト」リンク
  • 「ユーザー設定」リンク
  • 「ユーザー一覧」リンク
  • 「プロフィール表示」リンク

→テスト駆動開発の流れとしてはここで統合テストを書くところだけど後でとこのと

logged_in?メソッドを定義

#レイアウトのリンクを変更するコードの例
<% if logged_in? %>
  # ログインユーザー用のリンク
<% else %>
  # ログインしていないユーザー用のリンク
<% end %>

このために必要な論理値を返すlogged_in?メソッドを定義していく
ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_userがnilではないという状態を指します。これをチェックするには否定演算子 (4.2.3) が必要なので、! を使っていきます

module SessionsHelper
  
  # 渡されたユーザーでcookieを使ってログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  
  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
  
  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end
end

logged_in?ヘルパーメソッドを利用して
ログインの有無によってlayoutが変わるようにヘッダー部分を書き換える

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        #ログインしていれば(current_userがnilでなければ)
        <% if logged_in? %>
          #10章で実装
          <li><%= link_to "Users", '#' %></li>
          #ドロップダウンメニュー
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              #自分のプロフィールページへのリンク
              <li><%= link_to "Profile", current_user %></li>
              #10章で実装
              <li><%= link_to "Settings", '#' %></li>
              <li class="divider"></li>
              <li>
                #ログアウト
                <%= link_to "Log out", logout_path, method: :delete %>
              </li>
            </ul>
          </li>
        #ログインしていない場合
        <% else %>
          #ログインリンク
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

ドロップダウン機能を有効にするため、Railsのapplication.jsファイルを通して、Bootstrapに同梱されているJavaScriptライブラリとjQueryを読み込むようアセットパイプラインに指示します

・
・
・
//
//= require rails-ujs
//= require jquery
//= require bootstrap
//= require turbolinks
//= require_tree .

ログインパスにアクセスして有効なユーザー (ユーザー名がexample@railstutorial.org、パスワードがfoobar) としてログインできるようになっているので、これまでの3つのセクションのコードを効率よくテストできるようになります

また、ブラウザを閉じるとcookieが消去され、再度ログインを要求されるようになっている
→クラウドIDE利用の場合はそうならないっぽい(他のブラウザを使って試しましょうとの事)

演習

  1. ブラウザのcookieインスペクタ機能を使って (8.2.1.1)、セッション用のcookieを削除してみてください。ヘッダー部分にあるリンクは非ログイン状態のものになっているでしょうか? 確認してみましょう。
  2. もう一度ログインしてみて、ヘッダーのレイアウトが変わったことを確認してみましょう。その後、ブラウザを再起動させ、再び非ログイン状態に戻ったことも確認してみてください。注意: もしブラウザの [閉じたときの状態に戻す] 機能をオンにしていると、セッション情報も復元される可能性があります。もしその機能をオンにしている場合、忘れずにオフにしておきましょう

動作確認のみにて省略

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

ログインの統合テスト

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

fixture

テスト用の初期データ(ざっくり)

自分でfixtureファイルを作成してデータを追加する

有効な名前とメールアドレス、パスワードを用意する
Sessionsコントローラのcreateアクションに送信されたパスワードと比較できるようにする必要もある
→password_digest属性をユーザーのfixtureに追加

digestメソッドを独自に定義する

has_secure_passwordでbcryptパスワードが作成されるので、同じ方法でfixture用のパスワードを作成します。

#secure_passwordのソースコードのパスワード生成部分
#string→ハッシュ化する文字列 cost→ハッシュを算出するための計算コスト
BCrypt::Password.create(string, cost: cost)

#テスト中は最小で本番環境ではしっかりなコストパラメータの計算的な事
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                              BCrypt::Engine.cost

上記を利用したfixture向けのdigestメソッドをUserモデルに追加

・
・
・
  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
end

digestメソッドを定義したので有効なユーザーを表すfixtureが作成できるようになる

michael:
  name: Michael Example
  email: michael@example.com
  #fixtureではERbを利用できる
  password_digest: <%= User.digest('password') %>

has_secure_passwordで必要となるpassword_digest属性はこれで準備できましたが、ハッシュ化されていない生のパスワードも参照できると便利です。しかし残念なことに、fixtureではこのようなことはできません。

また、上記のusers.ymlにpassword属性を追加するとエラーが発生する(DBに「password」というカラムが存在しないため)
なので、よく使う手法としてテスト用のfixtureでは全員同じパスワード「password」とする

#有効なユーザー用のfixtureを作成したのでテストでfixtureのデータを参照できる
#users→fixtureのファイル名users.yml
#:michael→users.ymlで設定したユーザーを参照するためのキー
#userに users.yml内の:michaelのデータを代入
user = users(:michael)

ログインのテスト

アクセス可能なユーザーデータが出来たのでレイアウトのリンクのテストを定義する
(前述の手順をコードに落とし込む)

require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  
  #fixtureのユーザーを読み込む
  def setup
    @user = users(:michael)
  end
・
・
・
  test "login with valid information" do
    #login_pathにgetのリクエスト
    get login_path
    #login_pathにposuのリクエスト 内容→params: { session: { email: @user.email, password: 'password' } }
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    #ユーザー詳細画面にリダイレクトされる
    assert_redirected_to @user
    #実際にリダイレクト先に移動
    follow_redirect!
    #sers/showが描写される
    assert_template 'users/show'
    #login_pathへのリンクの数が0である
    assert_select "a[href=?]", login_path, count: 0
    #logout_pathへのリンクがある
    assert_select "a[href=?]", logout_path
    #user_path(@user)へのリンクがある
    assert_select "a[href=?]", user_path(@user)
  end
end

演習

  1. 試しにSessionヘルパーのlogged_in?メソッドから!を削除してみて、リスト 8.23が redになることを確認してみましょう。
  2. 先ほど削除した部分 (!) を元に戻して、テストが greenに戻ることを確認してみましょう。

1.!を取るとユーザーがnilの時trueになるためRED
2.戻せば戻る

8.2.5 ユーザー登録時にログイン

以上で認証システムが動作するようになりましたが、今のままでは、登録の終わったユーザーがデフォルトではログインしていないので、ユーザーがとまどう可能性があります。

ユーザー登録時にログインする設定

・
・
・
  def create
    @user = User.new(user_params)
    if @user.save
      #sessions helperで定義したlog_inメソッド
      log_in @user
      flash[:success] = t('.welcome_message')
      redirect_to @user
    else
      render 'new'
    end
  end
・
・
・

ユーザー登録時にログインする動作のテスト

logged_in?ヘルパーメソッドとは別に、is_logged_in?ヘルパーメソッドを定義する
→取り違えやら何やら何やらを防ぐために名前をかぶらせないように気を付ける!

残念ながらヘルパーメソッドはテストから呼び出せないので、リスト 8.18のようにcurrent_userを呼び出せません。sessionメソッドはテストでも利用できるので、これを代わりに使います。

・
・
・
  fixtures :all
  include ApplicationHelper

  # Add more helper methods to be used by all tests here...
  # テストユーザーがログイン中の場合にtrueを返す
  def is_logged_in?
    #sessionの:user_idがnilではない
    !session[:user_id].nil?
  end
end

is_logged_in?ヘルパーメソッドを使って
ユーザー登録後にログイン状態になっているかをテストする

・
・
・
  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'users/show'
    assert_not flash.empty?
    #テストユーザーがログインしている
    assert is_logged_in?
  end
end

演習

  1. リスト 8.25のlog_inの行をコメントアウトすると、テストスイートは red になるでしょうか? それとも green になるでしょうか? 確認してみましょう。
  2. 現在使っているテキストエディタの機能を使って、リスト 8.25をまとめてコメントアウトできないか調べてみましょう。また、コメントアウトの前後でテストスイートを実行し、コメントアウトすると red に、コメントアウトを元に戻すと green になることを確認してみましょう。ヒント: コメントアウト後にファイルを保存することを忘れないようにしましょう。また、テキストエディタのコメントアウト機能については Test Editor Tutorial の Commenting Out (英語) などを参照してみてください。

1.log_inの行→コメントアウトするとユーザー登録直後にログイン状態にならないのでRED
2.[ctrl+A]で全選択→[ctrl+/]でまとめてコメントアウト(Win)→保存→RED→戻す→GREEN

まとめとか感想

ログインの仕組みを作る
ユーザー登録時にそのままログイン状態になるって言うのは大事!と、思いました🐫

ちゃんと頭でわかりながら進められているか不安だけど
分からない所はメモってるし、後で気づいたら書き足せばいいから良し!とする!!

らくだ🐫にもできるRailsチュートリアルとは

「ド」が付く素人のらくだ🐫が勉強するRailsチュートリアルの学習記録です。
自分用に記録していますが、お役に立つことがあれば幸いです。

調べたとはいえらくだ🐫なりの解釈や説明が含まれます。間違っている部分もあるかと思います。そんな所は教えて頂けますと幸いなのですが、このブログにはコメント機能がありません💧お手数おかけしますがTwitterなどでご連絡いただければ幸いです