Pythonとサラリーマンと

2020年6月にPythonを始めたサラリーマンのブログです。

Rails Tutorial 第8章を噛み砕く

第8章で学べること

  • ログイン機能の実装方法

第7章ではユーザ登録機能の実装方法を学習した。
第8章ではDBに登録されているユーザを使ってログインを行う機能を実装する。
ログインした後にページを移動してもログインの状態が保てれるようにSessionメソッドを使用する。
これによりユーザログイン後に、そのユーザを使ってWebサービスを利用することができるようになる。

この記事は、筆者のRails Tutorial 第6章に関する備忘録。
実際に手を動かす時はRails Tutorialの流れに沿って行うこと。
なお、テストに関する記載は省いている。

Webサービスでログイン機能が担う役割

  1. ログインページ
  2. ログイン失敗時の処理
  3. ログイン成功時の処理
  4. ログイン情報の保持(ページ移動してもそのユーザであり続ける)
  5. ログアウトボタンと処理

下準備

ログイン処理はSessionsコントローラで実行する。
Sessionコントローラは、UserコントローラのようにModelを持つものではない。
SessionコントローラとSessionのViewで構成される。
↓新しいコントローラの作成

rails generate controller Sessions new

*rails generateでnewアクションを実行するとviewも作成される

↓rootの定義

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

これでSessionsコントローラを使用できるようになった。

ログインページを作成

↓ログインフォームはform_forを使ってこんな感じ。 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のpost先の指定方法 sessionリソースにはsessionモデルというものを用意していない。 userモデルの存在したユーザー登録フォームの場合は、

form_for(@user)

という記載をすれば良かった。
これによりRailsは「formのactionはusers/というURLへのPOST」ということを自動判別する。
*formのactionとはformタグのaction属性のこと
一方でログインフォームではモデルがないので@userというインスタンス変数に相当するものがない。
そのため↓のように指定する。

form_for(:session, url:login_path)

:sessionの部分はリソースの名前。url:の部分はPOST先のURL。

ログイン失敗の場合処理

↓ログイン情報が正しいかどうかを識別しするようにコードを書く。

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

↓まず、入力されたemail情報によりデータベースから値を取り出す。(find_byメソッド)

user = User.find_by(email: params[:session][:email].downcase)

↓次に、userが存在するのか、パスワードが正しいかをチェックする。(authenticateメソッド)

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

上記により「ユーザーがデータベースにあり、かつ、認証に成功した場合にのみ」にtrueを返すことができる。
そしてelseの下にはfalseだった時の処理を書く。
flashのキーを[:danger]を指定。かつ.nowを使って一度だけ表示の制限をつける。

 flash.now[:danger] = 'Invalid email/password combination'

*キーはBootstrapに対応したものを指定
flash単体ではなくflash.nowにすることで新しいリクエストが発生したらflashメッセージが消滅する

ログイン成功の場合の処理

ログインに成功した場合は、ページ移動してもそのログイン情報を保持しながらWebサービスを使用できるようにする。
そのためにRubyのモジュール機能を利用する。 SessionHelperモジュールにcookiesを利用したログイン情報保持のメソッドを作成する。
SessionHelperをApplicationコントローラにincludeしておけば、全コントローラから利用できるようになる。
app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

log_inメソッド

Railsに搭載されているsessionメソッドを使用するために用意する。
sessionメソッドを使用すればユーザIDをブラウザのCookies内に暗号化して保存できる。
sessionメソッドで作成する一時Cookiesはブラウザを閉じると消滅する。 同じログイン手法を様々な場所で使いまわせるようにSessionsHelperに登録する

module SessionsHelper

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

*sessionsメソッドで暗号化され保存される情報はセッションハイジャック不可

↓これをログイン成功後に使用する

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

これによりユーザ認証をクリアすればセッション情報がcookiesに保存され、かつユーザページに移動する処理が作れた。

current_userメソッド

  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

↑このメソッドは、Cookies内のに保存したログイン情報を取り出すために作成する。 これはもう、暗記した方が良さそう。
sessionに値が保存されている時に@current_userにデータベースの情報を保存しておける。
ポイント①:findではなくfind_byを使う
→findはユーザIDが存在しない場合に例外が発生する。  find_byならユーザIDが存在しない場合にnilを返す。

ポイント②:||=の演算子を使う
→これはrubyでよく使われる演算子とのこと。

以上、これでcurrent_userメソッドが使えるようになった。
sessionにユーザIDが保存されていれば@current_user情報を取り出せる。

ログインの有無によりページレイアウトを変更する

ステップ①:ユーザがログインしていることを確認するためのメソッドを作る

ログインしてる = sessionにユーザのidが存在している、という状態。 app/helpers/sessions_helper.rb

  def logged_in?
    !current_user.nil?
  end

このように否定演算子nil?でcurrent_userに値が入っているか確認する。

ステップ②:ヘッダーを改良する これで準備が整ったので、ヘッダーを記述しているlayoutsを修正する。
↓完成系はこんな感じ。
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>

ポイント1:if elseでloginしている場合としていない場合の条件分岐
ポイント2:css(bootstrap)でドロップダウンのデザインを定義する
→dropdownクラスやdropdown-menu
ポイント3:dropdown機能を有効にするためにapplication.jsを変更する →Bootstrapに同梱されているJavaScriptライブラリとjQueryを読み込むように
 アセットパイプラインに指示する
app/assets/javascripts/application.js

//= require jquery
//= require bootstrap

この2行を追加する。

ユーザー登録したら自動的にログインする機能

すでにlog_inメソッドを作成しているので、それを利用すればユーザ作成後に自動的にログインするようにできる。
app/controllers/users_controller.rb

  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

このようにlog_in @userを1行追加。

ログアウト機能

ステップ1:sessionからユーザIDを削除するメソッドを作る

session.delete(:user_id)

これで消せる。

ステップ2:上記を使ってログアウトメソッドを作る
app/helpers/sessions_helper.rb

  def log_out
    session.delete(:user_id)
    @current_user = nil
  end

↑sessionからidを削除して、かつ@current_userの情報もnilに更新する。

最後に、コントローラのdestroyアクションに追記して完了↓。
app/controllers/sessions_controller.rb

 def destroy
    log_out
    redirect_to root_url
  end

↑ログアウトしたらhomeに戻るようになっている。

以上
こんな感じでログイン・ログイン情報の利用・ログアウトを実装できるようになった。