Rails Tutorial 第八章 簡易まとめ セッション

セッションについてならっていくぞ

定義などの解説はこちらhttps://qiita.com/krppppp/items/f80b66913e0c8b2566ca

8.1.1までのことがかいてあるので8.1.2から~~

8.1.2 ログインフォーム

しいセッションで使うビュー、つまりログインフォームを整えましょう
そこでログインフォームとユーザー登録フォームにはほとんど違いがないことがわかります。違いは、4つあったフィールドが [Email] と [Password] の2つに減っていることだけです
image.png

app/views/sessions/new.html.erb
 <% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>#新規ユーザーのためのリンク
  </div>
</div>
<%= form_for(:session, url: login_path) do |f| %>

上のコードで、リソースの名前とそれに対応するURLを具体的に指定する必要があります

すると生成されたHTMLフォームは

<form accept-charset="UTF-8" action="/login" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="authenticity_token" type="hidden"
         value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
  <label for="session_email">Email</label>
  <input class="form-control" id="session_email"
         name="session[email]" type="text" />
  <label for="session_password">Password</label>
  <input id="session_password" name="session[password]"
         type="password" />
  <input class="btn btn-primary" name="commit" type="submit"
       value="Log in" />
</form>

・フォーム送信後にparams[:session][:email]とparams[:session][:password]ができる

8.1.3 ユーザーの検索と認証(入力が無効な場合の処理)

ひとまずコントローラを整理しましょう

app/controllers/sessions_controller.rb
 class SessionsController < ApplicationController

  def new
  end

  def create
    render 'new'
  end

  def destroy
  end
end

これでルーティングから、無効な場合createアクションからNewビューを表示

---
session:
  email: 'user@example.com'
  password: 'foobar'
commit: Log in
action: create
controller: sessions

上はデバック情報になるがここからパラメータはネストしたハッシュであると推測できる(らしい。よくわからんhttps://railstutorial.jp/chapters/rails_flavored_ruby?version=5.1#code-nested_hashes)

つまり

params{session: {password: "foobar" , email: "user@example.com"}}

入れ子の入れ子であり

params[:session][:password]

これで”use@example.com”が取得できるということである!!!
要するにcreateアクションの中では、ユーザーの認証に必要なあらゆる情報をparamsハッシュから簡単に取り出せるということ!!!理解してるか!?
このparamsでこんなことが可能!

app/controllers/sessions_controller.rb
 class SessionsController < ApplicationController


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

1,1行目で、送信されたメールアドレスを小文字にして(6.2.5の一意性の部分で小文字で保存しているから、確実にマッチさせるため)データベースから探してそのユーザーの情報すべてを取得、変数代入している
2,2行目を砕いて説明すると
入力されたメールアドレスを持つユーザーがデータベースに存在し、かつ(&&)入力されたパスワードがそのユーザーのパスワードである場合(IF)TRUE

8.1.4 flashを表示

ユーザー登録のエラーメッセージ表示にUserモデルのエラーメッセージをうまく利用したことを思い出しましょう。ユーザー登録の場合、エラーメッセージは特定のActive Recordオブジェクトに関連付けられていたのでその手が使えました。しかしセッションではActive Recordのモデルを使っていないため、その手が通用しません。そこで、ログインに失敗したときには代わりにフラッシュメッセージを表示することにします

理解できそうで出来ない。

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
      flash[:danger] = 'Invalid email/password combination' # 本当は正しくない
      render 'new'
    end
  end

  def destroy
  end
end

ひとまずこれで表示はされるけど問題が、一度表示されると消えずにそのまま残ってしまうのである。

8.1.5 flashのテスト

上の問題はアプリケーションのちいさなバグである。
テストをかいて解決をめざす。統合テストーーーーーーーーーーーーー!

$ rails generate integration_test users_login
      invoke  test_unit
      create    test/integration/users_login_test.rb

これからの流れとしては
1 ログイン用のパスを開く
2 新しいセッションのフォームが正しく表示されたことを確認する
3 わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
4 新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する
5 別のページ (Homeページなど) にいったん移動する
6 移動先のページでフラッシュメッセージが表示されていないことを確認する

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
end

結果はREDでかえってきます。これを解決するため
flash.now[:danger] = 'Invalid email/password combination'
に書き換えを行う(flash.nowのメッセージはその後リクエストが発生したときに消滅します)

GREENへ

テストの豆知識

$ rails test test/integration/users_login_test.rb
なお、上の例のように、rails testの引数にテストファイルを与えると、そのテストファイルだけを実行することができます。

8.2 ログイン

無効な送信のあとは有効な送信。つまりログインをフォームで扱えるようにする
cookiesを使った一時セッションでユーザーをログインできるようにします。このcookiesは、ブラウザを閉じると自動的に有効期限が切れるものを使います

モジュール機能

https://railstutorial.jp/chapters/rails_flavored_ruby?version=5.1#sec-back_to_the_title_helper
おびただしい数のメソッドを定義するので一カ所にまとめる機能
コントローラ生成時にファイルがつくられているのでそれを利用する

app/controllers/application_controller.rb
 class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

ヘルパーはビューにも自動的に読み込まれる+こうしてしまうことでどのコントローラでも使える様になる

8.2.1 log_in メソッド

sessionメソッド(sessionsコントローラは関係ないよ)

Railsで事前定義済みのsessionメソッドを使って、単純なログインを行えるようにします

app/helpers/sessions_helper.rb
 module SessionsHelper

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

このコードを実行するとユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成されます。この後のページで、session[:user_id]を使ってユーザーIDを元通りに取り出すことができます。

sessionメソッドで作成した一時cookiesは自動的に暗号化され、リスト 8.14のコードは保護されます。そしてここが重要なのですが、攻撃者がたとえこの情報をcookiesから盗み出すことができたとしても、それを使って本物のユーザーとしてログインすることはできないのです。ただし今述べたことは、sessionメソッドで作成した「一時セッション」にしか該当しません。
永続的セクションはまた今度

このコードをcreateアクションで完了してユーザープロフィールページへリダイレクト!!

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# ==user_url(user)
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

8.2.2 現在のユーザー(curret_userメソッド)

ユーザーIDを一時セッションの中に安全に置けるようになったので、今度はそのユーザーIDを別のページで取り出すためのcurret_userメソッドの定義をしていきます

目的としては、
<%= current_user.name %>

redirect_to current_user

このコードがかけるようにするわけである

app/helpers/sessions_helper.rb
 module SessionsHelper

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

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    @current_user ||= User.find_by(id: session[:user_id]) #find_by~~の部分はid検索をして「ユーザーがログインしてない」などの状態のときnilを返す
  end
end

or演算子「||」 をつかった表現

上のコードで注意するのが 演算子

もともと

def current_user
User.find_by(id: session[:user_id])
end

こちらが原型になるが、問題が1リクエスト内で何度も呼び出されると、呼び出された回数と同じだけデータベースにも問い合わせされてしまう点である。

そこでインスタンス変数に代入してよびだせば

if @current_user.nil?
@current_user = User.find_by(id: session[:user_id])
else
@current_user
end

ログインしてない場合のみcookiesからuser idを引っ張り出して代入
といった無駄のない記述になる

 

「||=」ってなんやねん

この「||=」(or equals) という代入演算子はRubyで広く使われているイディオムであり、Ruby開発者を志すならこの演算子に習熟することが重要

単純にいえば演算子の短縮形である
$ rails console

x = 1
=> 1
x += 1
=> 2
x *= 3
=> 6
x -= 8
=> -2
x /= 2
=> -1
これと同様うめこんでいるだけで意味合いが少し複雑に感じるだけである

Rubyでは、「変数の値がnilなら変数に代入するが、nilでなければ代入しない (変数の値を変えない)」という操作が非常によく使われます

 >> @foo
  => nil
  >> @foo = @foo || "bar" #nilの論理値はfalseになるので、@fooへの代入「nil || "bar"」の評価値は"bar"に
  => "bar"
  >> @foo = @foo || "baz"
  => "bar"

このプロセスを短縮すると
@foo = @foo || "bar"
こうなるよねって話

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

目的:ログインしたプロフィールページのリンクとログイン前のリンクの表示をそれぞれで変更iされるようにする

コードに落とし込むとこんな感じ??
rb:
logged_in? %>
# ログインユーザー用のリンク
<% else %>
# ログインしていないユーザー用のリンク
<% end %>

app/helpers/sessions_helper.rb
 module SessionsHelper



  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?#sessionのユーザーIDは無効 ”じゃない??”(否定演算子(!)をつかったらこの表現になる)
  end
end

ヘッダーパーシャルのリンクをif文を差し込みと名前付きルートをつかってととのえる

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 %>#定義したばかりのlog_outパスと引数としてHTTPのDELETEリクエスト
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>#ログインしてなければloginリンク
        <% end %>
      </ul>
    </nav>
  </div>
</header>

ここで謎のドロップダウンメニューがでてきた

Bootstrapのドロップダウンメニュー機能

Bootstrapに含まれるCSSのdropdownクラスやdropdown-menuなどを使っています
こちらの機能を有効にするために
Railsのapplication.jsファイルを通して、Bootstrapに同梱されているJavaScriptライブラリを読み込むよう、アセットパイプラインに指示
その際jQueryも追加する必要があります

app/assets/javascripts/application.js
 //= require rails-ujs
//= require jquery
//= require bootstrap
//= require turbolinks
//= require_tree .

試しに登録してあるユーザとしてログインして確認しときます
ブラウザをいったん完全に閉じると再度ログインを要求されることも確認を。

8.2.4 レイアウトのテスト

統合テストを用意して以下の順序でてすと

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

fixture (フィクスチャ)

上の変更を確認するためには、テスト時に登録済みユーザーとしてログインしておく必要があります。当然ながら、データベースにそのためのユーザーが登録されていなければなりません。

そこでこのfixtureを使って、テストに必要なデータをtestデータベースに読み込んでおくことができます

なんやの??→→YAML形式のデータをデータベースに流しこむための仕組みのことです。
テーブルを作成しただけでは、データを取得する際に確認したりするのが
効率が悪くなったりしますので。
そのためにテストデータを準備する必要があり、フィクスチャ機能を使います。

現時点のテストでは、ユーザーは1人いれば十分です。
そのユーザーには有効な名前と有効なメールアドレスを設定しておきます。テスト中にそのユーザーとして自動ログインするために、そのユーザーの有効なパスワードも用意して、Sessionsコントローラのcreateアクションに送信されたパスワードと比較できるようにする必要があります。のデータモデルから、password_digest属性をユーザーのfixtureに追加すればよいことが分かります。そのために、digestメソッドを独自に定義することにします。
このdigestメソッドは、今後様々な場面で活用します。例えば9.1.1でもdigestを再利用するので、このdigestメソッドはUserモデル (user.rb) に置いておきましょう

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 :#costはコストパラメータと呼ばれる値
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)#has_secure_passwordでbcryptパスワードが作成されるので、同じ方法でfixture用のパスワードを作成
  end
end

digestメソッドができたのでユーザーログインテストで使う有効なユーザーを表すfixtureを作成

test/fixtures/users.yml
 michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
#上のコードにあるように、fixtureではERbを利用できる点にご注目ください。

コストパラメータ

コストパラメータでは、ハッシュを算出するための計算コストを指定します。コストパラメータの値を高くすれば、ハッシュからオリジナルのパスワードを計算で推測することが困難になります。本番環境ではセキュリティ上重要です。

ですが今回の例ではテスト中は高くする必要がないのでdigestメソッドの計算はなるべく軽くしておきたいです。

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

詳しいことはまた今度

有効な情報をもつユーザーのログインテスト

有効なユーザー用のfixtureを作成できたので、テストでは次のようにfixtureのデータを参照できるようになります。

user = users(:michael)

上のusersはfixtureのファイル名users.ymlを表し、:michaelというシンボルはユーザーを参照するためのキーを表します。

前述の操作手順をテストに落とし込むと

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#ログインリンクが0であるかどうか
    assert_select "a[href=?]", logout_path #ログアウトリンクの有無
    assert_select "a[href=?]", user_path(@user)#プロフィールページリンクの有無
  end
end

テストはグリーンのはずです

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

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

  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

まさにこれだけ。

さて、テストしましょう。

サインアップテスト内でサインアップ完了後にユーザーがログイン中かどうかをチェックします。
そのために、一度定義したlogged_in?ヘルパーメソッドとは別に、is_logged_in?ヘルパーメソッドを定義しておくと便利です。
このヘルパーメソッドは、テストのセッションにユーザーがあればtrueを返し、それ以外の場合はfalseを返します 。
残念ながらヘルパーメソッドはテストから呼び出せないので、sessionヘルパーのようにcurrent_userを呼び出せません。
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

サインアップテストへ

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

8.3 ログアウト

ログインもあればログアウトもある
ログアウト用リンクは既に作成済みなので、ユーザーセッションを破棄するための有効なアクションをコントローラで作成するだけで済みます

これまで、SessionsコントローラのアクションはRESTfulルールに従っていました。newでログインページを表示し、createでログインを完了するといった具合です。セッションを破棄するdestroyアクションも、引き続き同じ要領で作成することにします。ただし、ログインの場合と異なり、ログアウト処理は1か所で行えるので、destroyアクションに直接ログアウト処理を書くことにします。この設計 (および若干のリファクタリング) のおかげで認証メカニズムのテストが行い易くなります。

ログアウトの処理とはセッションからユーザーIDを削除
であるので
session.delete(:user_id)

上で現在のユーザー==nilとなる。落とし込むと

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

となりコントローラの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

毎度のごとくテストを設定していくが、名前を"login with valid information followed by logout"
として前述してある「有効な情報のユーザーログイン」につづいてログアウトテストを行うといった形に落とし込んでいく

test/integration/users_login_test.rb
 require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
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)
        assert is_logged_in?

    end

  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

8章まとめ

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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.