Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
0
Help us understand the problem. What is going on with this article?
@s-yoshi210

Railsチュートリアル 第8章

More than 1 year has passed since last update.

セッション

HTTPはステートレス(状態を保持できない)なプロトコルです。
HTTPのリクエスト1つ1つは、それより前のリクエスト情報を利用することができません。
そのため、ブラウザのあるページから別のページに移動した時に、ユーザーのIDを保持しておく手段として、Webアプリケーションでは、セッションという半永続的な接続をコンピュータ間(ユーザーのパソコンのWebブラウザとrailsサーバなど)に別途設定します。

実装方法

Rails でセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。
cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。
cookiesは、あるページから別のページに移動した時にもされないので、ここにユーザーID などの情報を保存できます。
アプリケーションはcookies内のデータを使って、例えばログイン中のユーザーが所有する情報をデータベースから取り出すことができます。
Rails では、sessionメソッドを使い、一時セッションを作成します。この一時セッションは、ブラウザを閉じると自動的に終了します。
また、第9章では、もっと長続きするセッションの作り方を学びます。

HTTPリクエスト URL 名前付きルート アクション名 用途
GET /login login_path new 新しいセッションのページ(ログイン)
POST /login login_path create 新しいセッションの作成(ログイン)
DELETE /logout logout_path destroy セッションの削除(ログアウト)

ログインフォーム

ユーザー登録フォームでform_forヘルパーを使ったときは、ユーザーのインスタンス変数@userを引数に取っていました。

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>

上記のように書くだけで、「フォームのactionは/usersというURLへのPOSTリクエストである」と解釈してくれていました。
しかし、セッションにはSessionモデルというものがないので、@userのようなインスタンス変数に相当するものがありません。
そのため、ログインフォームでは下記のようにリソースの名前とそれに対応するURLを具体的に指定する必要がある。

form_for(:session, url: login_path)

ログインフォーム

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>

上記で生成したログインフォームの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ハッシュに入る値が、params[:session][:email]とparams[:session][:password]になることが推測できます。

ユーザーの検索と認証

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

上記では、送信されたメールアドレスを使い、データベースからユーザーを取り出しています。(ユーザー登録時にメールアドレスは全て小文字で登録しているので、確実にマッチさせるためにdowncaseメソッドを使っている)

find_byメソッドを使用しているので、ユーザー情報が存在する場合はユーザーオブジェクトが、存在しない場合はnilが返される。
Rubyではnilとfalse以外の全てのオブジェクトはtrueになる性質を考慮すると、&&の前後の組み合わせは下記の通りとなります。

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

ユーザーが存在しない場合にはnilが返され、falseになるため、その時点でこのif文はfalseとなり、右の式は実行されないため、エラーにはならない。
これがもし左の式が存在しなかったら、エラーになってします。

フラッシュ

flashとflash.nowの違い

flash

次のアクションまで表示され、その次のアクションでは消える。
通常flashはリダイレクトを伴って利用するので、登録後にリダイレクトされた画面でメッセージが表示されており、次のアクションが実行された時点で消える。

flash.now

次のアクションで消えます。
登録に失敗してrenderでテンプレートを表示した場合、次のアクションでは消えて欲しいので、flash.nowを使います。

ログイン

同じログイン手法を様々な場所で使い回せるようにするために、Sessionヘルパーにlog_inというメソッドを定義する。

sessions_helper.rb
module SessionsHelper

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

sessionメソッドで作成した一時cookieは自動的に暗号化されて保護される。
sessionメソッドで作成されあた一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了します。

log_inというメソッドを使い、ユーザのログインを行う。

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

上記では、リダイレクトを使っていますが

redirect_to user

これは、Rails では自動的に「user_url(user)」を変換しています。

現在のユーザー

現在のログイン中のユーザー情報を返すメソッドを定義します。

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

ユーザーIDが存在しない状態でfindメソッドを使うと例外が発生してしまうので、この場合はユーザーIDが存在しなかったら、nilを返してくれるfind_byメソッドを使います。

@current_user = @current_user || User.find_by(id: session[:user_id])
@current_user ||= User.find_by(id: session[:user_id])          ⇦上記の短縮形

上記の意味は、@current_userに何も代入されていないときだけfind_byメソッドを呼び出しが実行されているので、既に@current_userにユーザ情報が代入されている場合はデータベースへの無駄な問い合わせが行われない。

テスト

fixture

テスト用のデータを作成できる。

テストユーザー用意

fixture向けのdigestメソッドを追加する

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を作成する

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

有効な情報を使ってユーザーログインテスト

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
user = users(:michael)

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

assert_redirected_to user_url(@user.id)
assert_redirected_to @user         ⇦短縮形

上記はリダイレクト先が正しいかをチェックしている。

follow redirect!

指定のページに実際に移動する。

assert_select "a[href=?]", login_path, count: 0

count: 0というオプションをassert_selectに追加すると、渡したパターンに一致するリンクが0件かどうかを確認している。

ログアウト

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
0
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
0
Help us understand the problem. What is going on with this article?