LoginSignup
0
0

More than 3 years have passed since last update.

【第8章】Railsチュートリアル 5.1(第4版) 基本的なログイン機構

Last updated at Posted at 2020-01-10

大まかな流れの自己整理が目的(コード外文章中のSession(s)の表記などはスルー状態)のため、内容そのものに不足・誤り等あれば追記&訂正していきますのでご指摘頂けますと幸いです:bow_tone1:
なお、筆者はYassLabさんの動画版で学んでいるため、本記事は「チュートリアル sample_app」+「他補足」個人的に「電子ページ以上に分かりやすい!」と感じた解説部分+参考記事を整理してみようと試みた劣化の内容寄りになってます。

8.1 セッション

セッション(Session)とは

ユーザがログイン後、ページ遷移しても再度ログインしなくてもいいように、「ログインしている」ことを記憶しておく機能のこと。
ページを移動しても変数の内容を保持する仕組みなので、ログイン機能以外でも利用可能。

Sessionsリソース

「ログインしているかどうか?」が知りたく、いちいちDB書き換えは面倒...
→ 今回はモデル(兼DB)は使わない!
Sessionやrailsサーバに一時的に情報を保持するやり方や、クライアント側で保持するCookieもあるので、今回はサーバ(に🔑)とブラウザに保存する。

railsサーバは「$rails server」 → 「ctrl c」で落ちてしまう...

参考
【Rails入門説明書】sessionについて解説
Railsのリソースとルーティングについて

 
まずはルーティング作成のため、トピックブランチにチェックアウト&Sessionsコントローラを生成する。

$ git checkout -b basic-login
$ rails generate controller Sessions new

 
Usersリソースの時は専用のresourcesを使ってルーティングを自動的にフルセットで利用できるようにしたが、Sessionリソースではフルセットはいらないので、「名前付きルーティング」だけを使うためルーティングに追加する。

routes.rb
get    '/login',   to: 'sessions#new'
post   '/login',   to: 'sessions#create'
delete '/logout',  to: 'sessions#destroy'

サーバ起動して、「/login」にアクセスするとnewアクションのview画面が出る。

スクリーンショット 2020-01-05 14.35.59.png

Sessionsコントローラのテストで名前付きルートを使うようにする( 「get sessions_new_url」 → 「get login_path」へ変更)。テストしてGREEN。

sessions_controller_test.rb
require 'test_helper'

# 「::」 は 「ActionDispatch/IntegrationTest」 ディレクトリみたいなイメージ
class SessionsControllerTest < ActionDispatch::IntegrationTest
  test "should get new" do
    get login_path
    assert_response :success
  end

end

 

ログインフォーム
ユーザコントローラ(users_controller.rb)では、モデルがあって、それを@user:インスタンス変数に格納→それをform_forに渡す→newアクションテンプレートが動き出した。

users_controller.rb
  def new
    @user = User.new
    # => form_for @user
  end

ただSessionではモデル作ってないからできない。
POSTリクエストを「/login」に送ってcreateアクションが動くメソッドが欲しいので、sessionsのコントローラ、ビュー画面に下記を追加する。

sessions_controller.rb
class SessionsController < ApplicationController
  # GET /login
  def new
    # POST /login => create action
  end

  # POST /login
  def create
  end
end

 
なお、form_forの引数には@userを入れなくてもシンボル(:session)で対応可能であり、引数にurlを加えればok

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">
    <!-- ↓↓↓ params[:session][:email] -->
    <!-- ↓↓↓ params[:session][:password] -->
    <%= 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>

参考
POSTメソッド

 
スクリーンショット 2020-01-05 16.56.52.png

createメソッドに追記(今回はインスタンス変数@userでなくローカル変数userに情報を入れる)

def create
      #まずはUser情報が必要
    user = User.find_by(email:params[:session][:email])
      #=> User object or false
      # (【rubyの仕組み】falseとnil以外はtrue)
    if user.authenticate(params[:session][:password])
      # Sucess
    else
      # Failure (sessionモデルがないのでバリデーションが使えない)
      flash[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  
ただ、この時点では「登録されていない(間違った)emailアドレスをパラメータに渡す(ユーザが入力する)と発生するエラー」がある。
find_byが見つからない、nil(nilオブジェクト:authenticateが適応されてない) がuserに入ってauthenticateメソッド → NoMethodError発生(メソッドが見つからない)

スクリーンショット 2020-01-05 17.51.08.png

ここで真っ先に思いつく(筆者のかろじての発想)のが「if user == nil...」などの書き方だが、
Rubyの特徴としてuserにユーザオブジェクトが入ればtrue、nilが入ればfalseを利用すれば、
&&(ex. a && b 意:「aがtrueでかつ、bもtrueだったらtrue結果、どちらかfalseだったらfalse結果」 )を使って下記に表現できる。
ちなみに、rubyに限らずコンピュータ言語の&&では、左側(今回のuser)がfalseの場合は右側(user.authenticate)は判断してもしなくても必然的にfalseになるので機械側が判断せず省略する(処理を止める)。

if user && user.authenticate(params[:session][:password])

参考
演算子式(Ruby 2.7.0 リファレンスマニュアル)

 
ここで小さなバグとして、エラーメッセージのflashの表示時間が長い(思ったより生き延びている..?)問題がある。

スクリーンショット 2020-01-05 20.23.46.png

原因として、flashの生存期間としては、次のリクエストが来るまで
前回がredirectに対して今回はrender(リクエストを発行するのでなく、「このテンプレートを描画してね」)、つまり、失敗してflash登場後に次のリクエストが来るまで残り続ける
①表示(0リウエスト目)、②リロード(1リクエスト目)、③リロード(2リクエスト目)

すぐに直さず回帰バグを防ぐため、統合テストを行う。

$ rails generate integration_test users_login
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

 
ログインに失敗したときのテスト(意図的にログインを失敗させたときの動作)flashのバグも再現させる
1.login_pathにgetリクエストを送る
2.sessionsのnewテンプレートが呼び出される
3.login_pathに失敗させる空のパラメータ(params)を送る
4.(たぶん失敗してるので)またnewテンプレート再描画
5.invalid~のflashが出てるか?
6.その後さらにtopページにリクエスト
7.その時にflashがないか?(消えてるか?)
 

この時点ではrailsテストは失敗(RED:正しい)。
直すにはcreateメソッドのflashにメソッドとしてnowを追記する。
 


# ユーザーログイン後にユーザー情報のページにリダイレクトする
flash.now[:danger] = 'Invalid email/password combination'

  
これにてテスト通過(GREEN)。
  

8.2 ログイン

ヘッドの部分を下記のように変更する。
ログインしてる時  → ログアウト(の選択項目)表示
ログアウトしてる時  → ログイン(の選択項目)表示

Sessionsメソッド

Sessionsという特殊な変数(railsの機能)を使い、サーバ側に一時的に情報を保存する。
Sessionsのデータはviewとcontrollerで利用できる。
Sessionsが切れるまでの保存期間はRailsサーバが落ちるまで+ブラウザが生き残ってる(ex.ログイン後、タブやChromeを閉じる前)まで。
 

session[:user_id] = user.id

<参考>
【Rails】Sessionの使い方について
Session管理とRailsのcookie store
 
log_inメソッドと組み合わせる

app/helpers/sessions_helper.rb
module SessionsHelper

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

   

Applicationコントローラのcreateメソッドに「log_in」、newへのリダイレクトを加える。


      log_in user
      redirect_to user

 

ユーザがProfileページへ飛ぶ時、今ログインしているユーザが誰か?(ちゃんと別の人でなく自分のユーザ情報か?)の情報を持ってこないことにははじまらない。
 → Session情報からユーザ情報をもってくるようsessionヘルパーを編集する。

  

ユーザーIDを一時セッションの中に安全に置けるようになったので、今度はそのユーザーIDを別のページで取り出す。
 → current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。

app/helpers/sessions_helper.rb
  def current_user
    # (元祖)current_user = User.find(session[:user_id])
    # view側でも引き出せるようにインスタンス変数にする
    # (改訂1)@current_user = User.find(session[:user_id])
    # findは失敗したらエラーを返すログイン中にsessionが切れる可能性はあるので、
    # 失敗しても「nil」を返すfind_byを使う。
    # (改訂2)@current_user = User.find_by(id: session[:user_id])
    # @current_user = User.find_by(id: session[:user_id])

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

    # 「or」演算子を使いわずか1行で
    # @current_user = @current_user || User.find_by(id: session[:user_id])

    # さらにRubyっぽくして完成(よく使われる形らしい)
    @current_user ||= User.find_by(id: session[:user_id])

  end

 
セッションにユーザーIDが存在しない場合、このコードは単に終了して自動的にnilを返す(何度もDB問い合わせなく処理が早い)。

レイアウトのリンクを変更する方法として、ヘルパーにメソッドを追加する。

sessions_helper.rb
 # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end

  
ヘルパーヘッダーのhtml記入。setting(設定)はダミーリンクに。
link_to はデフォルトの振る舞いとして、引数にユーザオブジェクト(cuurent_user)が加わったらユーザのProfieページに飛ばす。
Log outでdeleteのリクエストを送る。

<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>
            <!--元<li><%= link_to "Log in", '#' %></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>

補足(エラー)

ここでログインしようとしてエラー
sessionsコントローラでlog_inメソッドが見つからない。

スクリーンショット 2020-01-09 17.34.41.png

ヘルパーはもともとviewで使われる。ただ、今回はコントローラでヘルパーを使いたいので、module(ヘルパー側)と対応するincludeを追記する。

sessions_controller.rb
class SessionsController < ApplicationController
  #moduleと対応させる
  include SessionsHelper

# 省略

  
Bootstrapの機能を使うため、jsに追加する。

app/assets/javascripts/application.js
//= require jquery
//= require bootstrap

もう一度リロードするとログイン状態になり、右上の項目が変わる(プルダウンの起動もok)。

スクリーンショット 2020-01-09 18.06.04.png

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

fixture(テストを実行、成功させるための状態や前提条件の集合)をYAMLで用意する。

YAMLファイル

YAMLファイルとは、
構造化されたデータを表現するためのデータ形式のひとつ(HTML,XMLなどいろんなファイルの書き方のひとつ)。
配列(先頭に「-」)、ハッシュ(キー:値)、スカラー(文字列、数値、真偽知など)で構成される。

< 主な用途 >
・各種設定ファイル
・データ保存用 (シリアライゼーション)
・データ交換用フォーマット
・ログファイル

<参考>
YAMLとは|「分かりそう」で「分からない」でも「分かった」気に ...
プログラマーのための YAML 入門 (初級編)
【Ruby入門】YAMLの使い方をわかりやすく解説!

 
name,email,password_digest(あとでBCryptでハッシュ化)を含むYAMLを作成する。ラベルはmichael

test/fixtures/users.yml

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

 
3項演算子の作りとしては、
nilかfalseであれば(「:」から)下側のcost(精査)が、
「?」の左側の「min_cost」がそれ以外であれば上側の 「MIN_COST(簡易チェック)」が選択される。

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

 
統合テストに成功してたときのメソッド・テスト文を追加する。

test/integration/users_login_test.rb

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' } }
    # @userにリダイレクトされているかチェックしてされれば以下が動く
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    # count:0は「そのリンクが存在しないよね?」のチェックができる
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
  end

先ほど作ったfixtureのサンプルデータにはラベルにmichaelを入れたので呼び出しができる、

さらにsignup(ユーザ登録)し終えたらログインの過程はスキップしていいので、ユーザコントローラのリファクタリングとして下記を追記する。
① createアクションに保存成功時log_inへ
② ヘルパーから使うのでincludeを冒頭に

app/controllers/users_controller.rb
class UsersController < ApplicationController
  include SessionsHelper  #=> ヘルパー連動

省略

 def create
    @user = User.new(user_params)
      if @user.save
      log_in @user #=> ユーザー登録時にログイン
      flash[:success] = "Welcome to the Sample App!"

 
ただ、このタイミングで usersコントローラとsessionsコントローラの両方でApplicationControllerから継承されている(下記)ので DRY

 < ApplicationController

双方のincludeを消してapplicationコントローラのみincludeを追加する。

application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper

省略

テストのヘルパーメソッド(追加)

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

 
assert(バグが無い様に条件式を埋め込んで、明示すること)追加。

test/integration/users_signup_test.rb
#showテンプレート表示後
assert is_logged_in? #=> signup 終えた人はログインも終わってるか?

8.3 ログアウト

ユーザが「Log out」をクリックしたらDELETEアクションが反応するようにする。

<大まかな流れ>
1.セッションからユーザーIDを削除(ユーザのログアウト)
2.ヘルパーにlog_outメソッド、sessionsコントローラにdeleteメソッドの実装
3.統合テスト

app/helpers/sessions_helper.rb
# 現在のユーザーをログアウトする
  def log_out
    # キーを指定すると、キーに該当するバリュー(今回はユーザid)を削除してくれる
    session.delete(:user_id)
    @current_user = nil
  end

 
Sessionsコントローラにdestroyアクション追加(DELETEリクエストがlog_outアクションにきたら動く、rootにリダイレクト)。

app/controllers/sessions_controller.rb
  # DELETE /logout
  def destroy
    log_out
    redirect_to root_url
  end

この時点でページに行き「Log out」をクリックするとホームに戻ってる。

ログアウトのテストも記述するが、ログインのテストの末尾に追記していく。

test/integration/users_login_test.rb
# 分かるよう下記""の末尾に「 followed by logout(ログアウトもしてるよ)」追加
   test "login with valid information followed by logout" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    # ログインしてるよね?追記(なくてもok)
    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に送りつける
    delete logout_path
    # sessions情報が消えるので、ログインしてないですよね?
    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

一通り終了!!

herokuへデプロイして、ログアウト動作の確認。

$ git checkout master
$ git merge basic-login
$ git push heroku master

スクリーンショット 2020-01-10 19.45.38.png

「Log out」クリックして、

スクリーンショット 2020-01-10 19.49.37.png

無事ホーム(root)に戻ったのでok!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0