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
config/routes.rbRails.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.rbclass 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に置き換えます。後者は、レンダリングが終わっているページで特別にフラッシュメッセージを表示することができます。
test/integration/users_login_test.rbrequire '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
rails test test/integration/users_login_test.rb
8.2 ログイン
app/controllers/application_controller.rbclass ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHeler
end
8.2.1 log_inメソッド
log_inメソッドapp/helpers/sessions_helper.rbmodule SessionsHelper
  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end
app/controllers/sessions_controller.rbclass 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を調べる方法が分からない? 今こそググってみるときです!
- ページの一番下にある「詳細設定」をクリック。
- 「コンテンツの設定」をクリック。
- 「Cookie」をクリック。
- 「すべての Cookie とサイトデータを表示」をクリック。
- ドメイン別にcookieが表示されます。
2:先ほどの演習課題と同様に、Expiresの値について調べてみてください。
1と同じ手順で見ることができます。
8.2.2 現在のユーザー
current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにします。
app/helpers/sessions_helper.rbmodule 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.rbmodule 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>
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 レイアウトの変更をテストする
- ログイン用のパスを開く
- セッション用パスに有効な情報をpostする
- ログイン用リンクが表示されなくなったことを確認する
- ログアウト用リンクが表示されていることを確認する
- プロフィール用リンクが表示されていることを確認する
app/models/user.rbclass 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
test/fixtures/users.ymlmichael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
test/integration/users_login_test.rbrequire '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
rails test test/integration/users_login_test.rb
演習
1:試しにSessionヘルパーのlogged_in?メソッドから!を削除してみて、リスト 8.23が redになることを確認してみましょう。
!を削除してTest
2:先ほど削除した部分 (!) を元に戻して、テストが greenに戻ることを確認してみましょう。
もとに戻してTest
8.2.5 ユーザー登録時にログイン
ユーザー登録中にログインするには、Usersコントローラのcreateアクションにlog_inを追加するだけで済みます。
app/controllers/users_controller.rbclass 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.rbrequire '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になるはずです。
rails test
演習
1:リスト 8.25のlog_inの行をコメントアウトすると、テストスイートは red になるでしょうか? それとも green になるでしょうか? 確認してみましょう。
RED
2:現在使っているテキストエディタの機能を使って、app/controllers/users_controller.rbをまとめてコメントアウトできないか調べてみましょう。また、コメントアウトの前後でテストスイートを実行し、コメントアウトすると red に、コメントアウトを元に戻すと green になることを確認してみましょう。
ヒント: コメントアウト後にファイルを保存することを忘れないようにしましょう。また、テキストエディタのコメントアウト機能については『開発基礎編: テキストエディタ』の 「コメントアウト機能」などを参照してみてください。
コメントアウトして動作確認するだけ
8.3 ログアウト
ログアウトの処理では、app/helpers/sessions_helper.rbのlog_inメソッドの実行結果を取り消します。つまり、セッションからユーザーIDを削除します。そのためには、次のようにdeleteメソッドを実行します。
log_outメソッドapp/helpers/sessions_helper.rbmodule 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.rbclass 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
test/integration/users_login_test.rbrequire '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
rails test
演習
1:ブラウザから [Log out] リンクをクリックし、どんな変化が起こるか確認してみましょう。また、test/integration/users_login_test.rbで定義した3つのステップを実行してみて、うまく動いているかどうか確認してみましょう。
動作確認するだけ
2:cookiesの内容を調べてみて、ログアウト後にはsessionが正常に削除されていることを確認してみましょう。
cookiesの中身をログアウトして見るだけ

