基本的なログイン機構から発展的なログイン機構 | rails チュートリアル 15

Ruby on Rails チュートリアル:実例を使って Rails を学ぼう第8章 基本的なログイン機構から第9章 発展的なログイン機構までの復習のメモです。

ここまでの復習

ここまでは、静的なページ作成からユーザー登録までの復習 | rails チュートリアル 14で行った。

基本的なログイン機構

ここからは、第8章 基本的なログイン機構です。

session

ログイン機能の作成には、sessionやcookiesといったメソッドや、機能をつかいます。

ということで、sessions controllerを作成します。

$ bundle exec rails generate controller Sessions new
Running via Spring preloader in process 73799
      create  app/controllers/sessions_controller.rb
       route  get 'sessions/new'
      invoke  erb
      create    app/views/sessions
      create    app/views/sessions/new.html.erb
      invoke  test_unit
      create    test/controllers/sessions_controller_test.rb
      invoke  helper
      create    app/helpers/sessions_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/sessions.coffee
      invoke    scss
      create      app/assets/stylesheets/sessions.scss

続いて、ルーティングです。ログインには、フォームを使用するので、get と postの両方を、
ログアウトを受けつけられるように、delete というものも使用します。

Rails.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'
  post '/signup',  to: 'users#create'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'

  resources :users
end

viewもひとまず、作成していく

<% provide(:title, 'Log in') %>
<div class="container">
  <div class="ta_center">
    <br>
    <br>
    <br>
    <h1 class="title_signup">Log in</h1>

    <%= form_with(scope: :session, url: login_path, html: {class: ''}, local: true) do |f| %>

      <table class="mr_auto ml_auto">
        <tr>
          <th><%= f.label :email %></th>
          <td><%= f.email_field :email, class: 'form-control' %></td>
        </tr>
        <tr>
          <th><%= f.label :password %></th>
          <td><%= f.password_field :password, class: 'form-control' %></td>
        </tr>
      </table>
      <div>
        <%= f.submit "Log in", class: "btn btn-primary" %>
      </div>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>

    <br>
    <br>
    <br>
  </div>
</div>

form_withの使い方は、ここを参照した。

optionが多い。。:scope。。。。うーん

routeには、new create destroyのメソッドへ関連付けられているので、controllerを修正

class SessionsController < ApplicationController
  def new
  end

  def create
    render 'new'
  end

  def destroy
  end
end

メールアドレスと、パスワードをraislでは、以下のようなコードで取得できます。

params[:session][:email]
params[:session][:password]

createでは、入力されたメルアドが、DBに登録されている何かしらのUserのメルアドとして探し出せるか、
かつ、一致したUserのパスワードも正しく入力されているか、といった認証をする。

探し出すには、User.find_by
PWの認証には、authenticate を使用する。

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.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

log_inメソッド

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

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

次に、すべてのページが読み込むapplication_controlle.rbにも、そのhelperを読み込ませる。

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

次に、定義したヘルパーメソッドを、セッションのcreateアクションで使用する。

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

ユーザー登録中にログインができるようにも、修正する。

class UsersController < ApplicationController
.
.
.
  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
.
.
.
end

現在ログイン中のユーザーを取得

sessions_helper にログイン中のユーザーを取得するためのメソッドを作成する

同時に、ログインしているかどうかのメソッドも作成する。

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

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

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

logged_in?メソッドを使用し、headerのリンクを修正する。

<header class="site_header">
  <div class="container">
    <small><%= link_to "site title", root_path, id: "logo" %></small>
    <nav>
      <ul>
        <li><%= link_to "About", about_path %></li>
        <li><%= link_to "Contact", contact_path %></li>
        <li><a href="http://news.railstutorial.org/">News</a></li>
        <% if logged_in? %>
          <li><%= link_to "Users", '#' %></li>
          <li><%= link_to "Profile", '#' %></li>
          <li><%= link_to "Settings", '#' %></li>
          <li><%= link_to "Log out", logout_path, method: :delete %></li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

log_outメソッド

log_inと同じようにログアウトを様々な場所で使い回せるようにするために、Sessionsヘルパーにlog_outという名前のメソッドを定義する。

module SessionsHelper
.
.
.
  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

作成したメソッドを、ログアウトを行うdestoryアクションがある、controllerで使用する。

class SessionsController < ApplicationController
.
.
.
  def destroy
    log_out
    redirect_to root_url
  end
end

ここまでで、

  • ユーザー登録と同時にログイン
  • ログインだけを行う
  • ログアウトだけを行う

が可能になった。

発展的なログイン機構

ここからは、第9章 発展的なログイン機構です。

Remember me 機能を作るのが目的になります。

必要となるremember_digest属性をUserモデルに追加

$ bundle exec rails generate migration add_remember_digest_to_users remember_digest:string
Running via Spring preloader in process 76148
      invoke  active_record
      create    db/migrate/20170718213739_add_remember_digest_to_users.rb

$ bundle exec rails db:migrate
== 20170718213739 AddRememberDigestToUsers: migrating =========================
-- add_column(:users, :remember_digest, :string)
   -> 0.0855s
== 20170718213739 AddRememberDigestToUsers: migrated (0.0856s) ================

その後、 トークン生成用メソッドというものを作成する

class User < ApplicationRecord
  attr_accessor :remember_token
  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

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end

sessions_helper.rbにもrememberメソッドを追記

module SessionsHelper
  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  
  # ユーザーを永続的セッションに記憶する
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end
.
.
.
end

その後、sessions_controller.rbで呼び出す。

class SessionsController < ApplicationController
.
.
.
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end
.
.
.
end

current_userメソッドを修正

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

この書き方では、一時セッションしか扱っていないので、このままでは正常に動作しないため修正する。

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

ユーザーを忘れる

しっかりログアウトできるように、Remember me 機能を無効化するメソッドを作成する

class User < ApplicationRecord
.
.
.
  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
end
module SessionsHelper
  # 永続的セッションを破棄する
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # 現在のユーザーをログアウトする
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end
end

複数のブラウザバグ対応

class SessionsController < ApplicationController
.
.
.
  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end
class User < ApplicationRecord
.
.
.
  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
.
.
.
end

viewにチェックボックスを表示させる

viewを修正し

    <%= form_with(scope: :session, url: login_path, html: {class: ''}, local: true) do |f| %>

      <table class="mr_auto ml_auto">
        <tr>
          <th><%= f.label :email %></th>
          <td><%= f.email_field :email, class: 'form-control' %></td>
        </tr>
        <tr>
          <th><%= f.label :password %></th>
          <td><%= f.password_field :password, class: 'form-control' %></td>
        </tr>
      </table>

      <div>
        <%= f.label :remember_me, class: "checkbox inline" do %>
          <%= f.check_box :remember_me, id: 'session_remember_me' %>
          <span>Remember me on this computer</span>
        <% end %>
      </div>

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

checkboxの値で、Remember me 機能をつかうかどうかを決められるようにする。

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
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

メモ

ひとまずは、同じことはできるようになった。(コピペだけなので、簡単)
時間を見つけて、解説などを読んでいこうと思う。

コメント

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

内容に問題なければ、下記の「コメントを送信する」ボタンを押してください。


  1. KATOON.NET
  2. TRASH
  3. 基本的なログイン機構から発展的なログイン機構 | rails チュートリアル 15