認証の習作: has_secure_password

外観

Action Cableの習作で作成したユーザ認証はログイン機能を省略していました。今回はログイン機能を中心にユーザ認証をフルスクラッチで作成したいと思います。
Rails 5.2の認証機能のチュートリアルを参考にさせていただきました。

環境
macOS 10.15.4
Ruby 2.7.1
Rails 6.0.3.1
Yarn 1.22.4
Node 13.12.0

参照
Authentication from Scratch with Rails 5.2 - Stefan Wintermeyer - Medium
Active Model の基礎 - Railsガイド > 1.11 SecurePasswordモジュール

リポジトリ
https://github.com/usutani/try_rails6_auth

アプリの準備

rails new -MT --skip-active-storage try_rails6_auth
cd try_rails6_auth

bin/rails g controller Home index

config/routes.rb

Rails.application.routes.draw do
  root 'home#index'
end

app/views/home/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Example</h1>
<p>Lorem ipsum dolor sit amet, consectetur</p>

has_secure_passwordの利用

Gemfile

# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7'

bundle install

ユーザの作成

bin/rails g scaffold User email:uniq password:digest
bin/rails db:migrate

app/models/user.rb

class User < ApplicationRecord
  has_secure_password
  validates :email, presence: true, uniqueness: true
end

bin/rails s
open http://localhost:3000/users/new

app/views/users/_form.html.erb
password_confirmationは省略可能。ある場合ない場合それぞれ適切に必須入力項目の確認が行われます。詳しくはRailsガイドを参照のこと。

画像1

セッションの作成

bin/rails g controller sessions new create destroy

app/controllers/sessions_controller.rb
session[:user_id]の代わりにcookies.encrypted[:user_id]を用います。

2021/12/04 追記 認証成功後 reset_session を呼ぶ。
Rails セキュリティガイド
最も効果的な対応策は、ログイン成功後に古いセッションを無効にし、新しいセッションidを発行することです。

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:email])
    if user&.authenticate(params[:password])
      reset_session
      cookies.encrypted[:user_id] = user.id
      redirect_to root_url, notice: 'Logged in!'
    else
      flash.now[:alert] = 'Email or password is invalid'
      render 'new'
    end
  end

  def destroy
    cookies.delete(:user_id)
    redirect_to root_url, notice: 'Logged out!'
  end
end

app/views/sessions/new.html.erb
form_withに変更しました。

<p id="alert"><%= alert %></p>

<h1>Login</h1>

<%= form_with url: sessions_path, local: true do |form| %>
 <div class="field">
   <%= label_tag :email %>
   <%= text_field_tag :email %>
 </div>
 <div class="field">
   <%= label_tag :password %>
   <%= password_field_tag :password %>
 </div>
 <div class="actions">
   <%= submit_tag "Login" %>
 </div>
<% end %>

補足: form_withでAjaxを用いる場合
https://github.com/usutani/try_rails6_auth/tree/remote-form_with

<%= form_with url: sessions_path do |form| %>

app/controllers/sessions_controller.rb
newからcreateに変更します。

    else
      flash.now[:alert] = 'Email or password is invalid'
      render 'create'
    end

touch app/views/sessions/create.js.erb

(() => {
  const el = document.getElementById('alert')
  el.innerText = '<%=j alert %>'
})()

ルーティング

config/routes.rb

Rails.application.routes.draw do
  root 'home#index'

  resources :users
  resources :sessions, only: %i[new create destroy]
  get 'signup', to: 'users#new', as: 'signup'
  get 'login', to: 'sessions#new', as: 'login'
  get 'logout', to: 'sessions#destroy', as: 'logout'
end

bin/rails s
open http://localhost:3000/login
open http://localhost:3000/logout

ユーザ名の表示

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  helper_method :current_user

  def current_user
    if cookies.encrypted[:user_id]
      @current_user ||= User.find(cookies.encrypted[:user_id])
    else
      @current_user = nil
    end
  end
end

app/views/home/index.html.erb

<% if current_user %>
  Logged in as <%= current_user.email %>.
  <%= link_to "Log Out", logout_path %>
<% else %>
  <%= link_to "Sign Up", signup_path %> or 
  <%= link_to "Log In", login_path %>
<% end %>

<p id="notice"><%= notice %></p>

<h1>Example</h1>
<p>Lorem ipsum dolor sit amet, consectetur</p>

画像2

以上です。

この記事が気に入ったらサポートをしてみませんか?