Rails-tutorial自分用まとめ(第8章 ログイン機構 主に演習とその回答)

その7から続く

8.1 セッション

HTTPはステートレス (Stateless) なプロトコルです。文字通り「状態 (state)」が「ない (less)」ので、HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われます。

ユーザーログインの必要なWebアプリケーションでは、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に別途設定します。

Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。

8.1.1 Sessionsコントローラ

Sessionsコントローラを生成する

rails generate controller Sessions new
リソースを追加して標準的なRESTfulアクションをgetできるようにする red config/routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
end
#セッションルールによって提供されるルーティング
HTTPリクエスト URL     名前付きルート アクション名      用途
GET          /login  login_path    new       新しいセッションのページ (ログイン)
POST         /login  login_path    create    新しいセッションの作成 (ログイン)
DELETE       /logout logout_path   destroy   セッションの削除 (ログアウト)

演習

1:GET login_pathとPOST login_pathとの違いを説明できますか?
少し考えてみましょう。

get    '/login',   to: 'sessions#new'
post   '/login',   to: 'sessions#create'

GET login_path: “/login”へアクセスされた際に”sessions#new”アクションを実行

POST login_path: “sessions#create”アクションの情報を”/login”へ送信。

Getは/loginのリクエストが来た際にsessionsコントローラのnewアクションをするという意味でPostはsessionsコントローラのcreateアクションを/loginに送信する

2:ターミナルのパイプ機能を使ってrails routesの実行結果とgrepコマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。
同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。
現在、いくつのSessionsリソースがあるでしょうか?

rails routes | grep users#
rails routes | grep sessions# 3つ

form_for(@user)

Railsでは上のように書くだけで、
「フォームのactionは/usersというURLへのPOSTである」と自動的に判定しますが、
セッションの場合はリソースの名前とそれに対応するURLを具体的に指定する必要があります。

form_for(:session, url: login_path)

8.1.2 ログインフォーム

演習

リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか?
ヒント:表 8.1とリスト 8.5の1行目に注目してください。

action=”/login” method=”post”から/loginにPostする場合
sessionsコントローラのcreateアクションが実行されるため

8.1.3 ユーザーの検索と認証

 ユーザーをデータベースから見つけて検証するapp/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      # エラーメッセージを作成する
      render 'new'
    end
  end

  def destroy
  end
end

User                   Password                                                a && b
存在しない        何でもよい (nil && [オブジェクト]) == false
有効なユーザー 誤ったパスワード (true && false) == false
有効なユーザー 正しいパスワード (true && true) == true

演習

1:Railsコンソールを使って、表 8.2のそれぞれの式が合っているか確かめて
みましょう. まずはuser = nilの場合を、次にuser = User.firstとした場合を
確かめてみて下さい。

ヒント:!!(user && user.authenticate(‘foobar’))

user = nil
>> !!(user && user.authenticate('password00'))
=> false
user = User.first
>> !!(user && user.authenticate('password00'))
=> true

8.1.5 フラッシュのテスト

rails generate integration_test users_login

1:ログイン用のパスを開く get login_path
2:新しいセッションのフォームが正しく表示されたことを確認する

assert_template ‘sessions/new’

3:わざと無効なparamsハッシュを使ってセッション用パスにPOSTする

post login_path, params: { session: { email: “”,password: “”,} }

4:新しいセッションのフォームが再度表示され、フラッシュメッセージが
追加されることを確認する

assert_template ‘sessions/new’

5:別のページ (Homeページなど) にいったん移動する

get root_path

6:移動先のページでフラッシュメッセージが表示されていないことを確認する

assert flash.empty?

flash.nowに置き換えます。後者は、レンダリングが終わっているページで特別にフラッシュメッセージを表示することができます。

フラッシュメッセージの残留をキャッチするテスト red
test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  test "login with invalid information" do
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email: "", password: "" } }
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end
テストしてredになることを確認
rails test test/integration/users_login_test.rb

8.2 ログイン

ApplicationコントローラにSessionヘルパーモジュールを読み込むapp/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHeler
end

8.2.1 log_inメソッド

log_inメソッドapp/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end
ユーザーにログインするapp/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

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

  def destroy
  end
end

演習

1:有効なユーザーで実際にログインし、ブラウザからcookiesの情報を調べてみてください。このとき、sessionの値はどうなっているでしょうか?

ヒント: ブラウザでcookiesを調べる方法が分からない? 今こそググってみるときです! 

Chromeでcookieを確認するには、「Google Chromeの設定」アイコン→「設定」をクリック。
  1. ページの一番下にある「詳細設定」をクリック。
  2. 「コンテンツの設定」をクリック。
  3. Cookie」をクリック。
  4. 「すべての Cookie とサイトデータを表示」をクリック。
  5. ドメイン別にcookieが表示されます。

2:先ほどの演習課題と同様に、Expiresの値について調べてみてください。

1と同じ手順で見ることができます。

8.2.2 現在のユーザー

current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにします。

セッションに含まれる現在のユーザーを検索するapp/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  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
end

演習

1:Railsコンソールを使って、User.find_by(id: …)で対応するユーザー
が検索に引っかからなかったとき、nilを返すことを確認してみましょう。

user = User.find_by(id: 10)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 10], ["LIMIT", 1]]
=> nil

2:先ほどと同様に、今度は:user_idキーを持つsessionハッシュを作成
してみましょう。

リスト 8.17に記したステップに従って、||=演算子がうまく
動くことも確認してみましょう

session = {}
=> {}
>> session[:user_id] = nil
=> nil
>> @current_user ||= User.find_by(id: session[:user_id])
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ? [["LIMIT", 1]]
=> nil
>> session[:user_id]= User.first.id
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> 1
>> @current_user ||= User.find_by(id: session[:user_id])
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2020-04-04 01:52:48", updated_at: "2020-04-04 01:52:48", password_digest: "$2a$10$X1rTgXx2saP84XHMnyVr5cM6iOKD...">

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

レイアウトのリンクを変更する方法として考えられるのは、
ERBコードの中でif-else文を使用し、
条件に応じて表示するリンクを使い分けることです。

<% if logged_in? %>
# ログインユーザー用のリンク
<% else %>
# ログインしていないユーザー用のリンク
<% end %>

このコードを書くためには、論理値を返すlogged_in?メソッドが必要なので、まずはそれを定義していきましょう。

logged_in?ヘルパーメソッドapp/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  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
ログイン中のユーザー用のレイアウトのリンクを変更するapp/views/layouts/_header.html.erb
<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>
        <% if logged_in? %>
          <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>
              <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>
Bootstrapに含まれるCSSのdropdownクラスやdropdown-menuなどを使っています。これらのドロップダウン機能を有効にするため、Railsのapplication.jsファイルを通して、Bootstrapに同梱されているJavaScriptライブラリとjQueryを読み込むようアセットパイプラインに指示します
application.jsにBootstrapのJavaScriptライブラリを追加するapp/assets/javascripts/application.js
//= require rails-ujs
//= require jquery
//= require bootstrap
//= require turbolinks
//= require_tree .

演習

1:ブラウザのcookieインスペクタ機能を使って、セッション用のcookieを削除してみてください。ヘッダー部分にあるリンクは非ログイン状態のものになっているでしょうか?

cookieを削除して確認するだけ

2:もう一度ログインしてみて、ヘッダーのレイアウトが変わったことを確認してみましょう。その後、ブラウザを再起動させ、再び非ログイン状態に戻ったことも確認してみてください。

注意: もしブラウザの [閉じたときの状態に戻す] 機能をオンにしていると、セッション情報も復元される可能性があります。もしその機能をオンにしている場合、忘れずにオフにしておきましょう

動作確認するだけ

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

  1. ログイン用のパスを開く
  2. セッション用パスに有効な情報をpostする
  3. ログイン用リンクが表示されなくなったことを確認する
  4. ログアウト用リンクが表示されていることを確認する
  5. プロフィール用リンクが表示されていることを確認する
テスト用データを作成するfixture向けのdigestメソッドを追加するapp/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
end
ユーザーログインのテストで使うfixture
test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
有効な情報を使ってユーザーログインをテストする green
test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with valid information" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
  end
end
テストして確認green
rails test test/integration/users_login_test.rb

演習

1:試しにSessionヘルパーのlogged_in?メソッドから!を削除してみて、リスト 8.23redになることを確認してみましょう。

 

!を削除してTest

2:先ほど削除した部分 (!) を元に戻して、テストが greenに戻ることを確認してみましょう。

もとに戻してTest

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

ユーザー登録中にログインするには、Usersコントローラのcreateアクションにlog_inを追加するだけで済みます。

ユーザー登録中にログインするapp/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end

is_logged_in?ヘルパーメソッドを定義しておくと便利です。
このヘルパーメソッドは、テストのセッションにユーザーがあればtrueを返し、それ以外の場合はfalseを返します。

残念ながらヘルパーメソッドはテストから呼び出せない。
sessionメソッドはテストでも利用できるので、これを代わりに使います。
ここでは取り違えを防ぐため、logged_in?の代わりにis_logged_in?を
使って、ヘルパーメソッド名がテストヘルパーとSessionヘルパーで同じにならないようにしておきます。

テスト中のログインステータスを論理値で返すメソッドtest/test_helper.rb

ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  fixtures :all

  # テストユーザーがログイン中の場合にtrueを返す
  def is_logged_in?
    !session[:user_id].nil?
  end
end

ユーザー登録後のログインのテスト green

test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  .
  .
  .
  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 is_logged_in?
  end
end

これで、テストを実行すると greenになるはずです。

 green
rails test

演習

1:リスト 8.25log_inの行をコメントアウトすると、テストスイートは red になるでしょうか? それとも green になるでしょうか? 確認してみましょう。

RED

2:現在使っているテキストエディタの機能を使って、app/controllers/users_controller.rbをまとめてコメントアウトできないか調べてみましょう。また、コメントアウトの前後でテストスイートを実行し、コメントアウトすると red に、コメントアウトを元に戻すと green になることを確認してみましょう。

ヒント: コメントアウト後にファイルを保存することを忘れないようにしましょう。また、テキストエディタのコメントアウト機能については『開発基礎編: テキストエディタ』の 「コメントアウト機能」などを参照してみてください。

コメントアウトして動作確認するだけ

8.3 ログアウト

ログアウトの処理では、app/helpers/sessions_helper.rblog_inメソッドの実行結果を取り消します。つまり、セッションからユーザーIDを削除します。そのためには、次のようにdeleteメソッドを実行します。

log_outメソッドapp/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end
ここで定義したlog_outメソッドは、Sessionsコントローラのdestroyアクションでも同様に使っていきます。
セッションを破棄する (ユーザーのログアウト)
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

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

  def destroy
    log_out
    redirect_to root_url
  end
end
ユーザーログアウトのテスト green
test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "login with valid information followed by logout" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end
テストして確認green
rails test

演習

1:ブラウザから [Log out] リンクをクリックし、どんな変化が起こるか確認してみましょう。また、test/integration/users_login_test.rbで定義した3つのステップを実行してみて、うまく動いているかどうか確認してみましょう。

動作確認するだけ

2:cookiesの内容を調べてみて、ログアウト後にはsessionが正常に削除されていることを確認してみましょう。

cookiesの中身をログアウトして見るだけ

8章のまとめ

1:Railsのsessionメソッドを使うと、あるページから別のページに移動
するときの状態を保持できる。一時的な状態の保存にはcookiesも使える

2:ログインフォームでは、ユーザーがログインするための新しいセッション
が作成できる

3:flash.nowメソッドを使うと、描画済みのページにもフラッシュメッセージ
を表示できる

4:テスト駆動開発は、回帰バグを防ぐときに便利

5:sessionメソッドを使うと、ユーザーIDなどをブラウザに一時的に保存できる

6:ログインの状態に応じて、ページ内で表示するリンクを切り替えることができる

7:統合テストでは、ルーティング、データベースの更新、レイアウトの変更が正しく行われているかを確認できる

その9に続く

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です