Rails Diary

プログラミングの学習記録です。

PFメモ日記(4) sorceryの復習

deviseを使おうと思ったけど、せっかくなのでsorceryの復習をしたい。

※ 内容ごちゃごちゃしてかなり分かりづらいです。

目次

1. sorceryをインストール

gem 'sorcery'
$ bundle exec rails g sorcery:install

create  app/models/user.rb
create  db/migrate/XXXXXXXXX_sorcery_core.rb

生成されたマイグレーションファイルにその他欲しい属性と制約を追加

# db/migrate/20220615092132_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :email,            null: false, index: { unique: true } # ユニークインデックスは生成時に最初から記載されていた
      t.string :crypted_password
      t.string :salt
      t.string :name, null: false # name属性とNOTNULL制約を追加

      t.timestamps                null: false
    end
  end
end

【メモ】 emailのユニークインデックスをadd_indexの書き方に変更したところ、emailに対してno method errorが出てしまったのでemailの箇所は生成された状態からいじらずにdb:migrateした。なんでエラーになったのかよく分からない。

crypted_passwordとsaltとは?

↓簡単に書くと

  • crypted_password(暗号化されたパスワードという意味)
  • saltとはハッシュ化する前のパスワード前後につける文字列のこと
    どういうこと?🤔

→パスワードをそのままDBに保存してしまうのはセキュリティ的な面で危険。このためハッシュ関数というものに通し、暗号化することでその安全性を高めている。ただし、このままだとハッシュ関数から元のパスワード文字列を割り出せてしまうかもしれないので、関数に通す前にsaltという文字列を結合した上でハッシュ関数に通すようにしている。

↓詳しく

関数とは

  • 何かを入れると(引数として渡すと)、何かを返してくれる(戻り値)プログラムの部品のこと

ハッシュ関数とは

  • 入力されたデータを「特定のルールに沿って」ぐちゃぐちゃにした値を返してくれる
  • 同じものを入れれば同じハッシュ値が帰ってくる
  • 適当な値に見えるが特定のルールに従って出力された値であり、デタラメな訳ではない

ハッシュ関数から返ってくる値(戻り値)がハッシュ値

  • ハッシュ値から元のデータは特定するのはほぼ不可能
  • パスワードなどそのままDBに保存したらセキュリティ的にアウトなものをハッシュ関数に渡して出てきたハッシュ値を保存している
  • 入力されたパスワードが正しいかチェックするときは、入力されたパスワードをハッシュ関数に入れ、出てきたハッシュ値とデータベースに保存されている値を比較する

  • このままだとハッシュ関数から元の値を割り出せてしまうかもしれない

  • 先人がさらに強固にするために、入力された値の前後に適当な文字列をくっつけてからハッシュ関数に入れる方法を考えた

→この文字列こそが「salt(ソルト)」

https://wa3.i-3-i.info/word16974.html

★ Sorceryでは、パスワードの末尾にランダムな文字列を結合し、その文字列をsaltフィールドに記憶することでパスワードハッシュの安全性を高めている

2. バリデーション(長い)

sorcery:installで追加されたUserモデルにバリデーションを追加する。

まずSorceryのログイン機能において、どういったバリデーションが追加されているのか確認する。

Sorceryのチュートリアルに記載されているバリデーションの例

# app/models/user.rb
class User < ActiveRecord::Base
  authenticates_with_sorcery!

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: true
end

→このif: -> { new_record? || changes[:crypted_password] }は何なのか

条件付きバリデーションとは?

特定の条件を満たす場合にのみバリデーションを実行したい場合があります。:ifオプションや:unlessオプションを使うことでこのような条件を指定できます。引数にはシンボル、ProcまたはArrayを使えます。:ifオプションは、特定の条件でバリデーションを行なうべきである場合に使います。特定の条件でバリデーションを行なうべきでない場合は、:unlessオプションを使います。
Active Record バリデーション - Railsガイド

  • 特定の条件の時のみ検証を行うことらしい
  • sorceryのバリデーションにおけるこれif: -> { new_record? || changes[:crypted_password] }は、バリデーションにつけられた条件
  • この条件の時だけバリデーション(検証)にかけてくださいね的なものだと思う

->(lambda記法)とは

呼び出したいProcオブジェクト:if:unlessで使うこともできます。Procオブジェクトを使うと、個別のメソッドを指定する代わりに、その場で条件を書けるようになりますワンライナーに収まる条件を使いたい場合に最適です。
Active Record バリデーション - Railsガイド

どういうこと?🤔

この辺り、そもそもProcやlambdaというものが分からないと先を理解するのが難しいと思った。

Procとlambda(ラムダ)とは?

【Ruby】ブロック・Proc・lambda を理解する - Qiita
↑こちらをかなり参考にしています。

Proc

  • do~endや{}で表現されるブロックを持ち運びに便利なオブジェクトにしたもの
  • Procはクラスであり、Proc.newでオブジェクトを作ることができる
  • 上記で生成されたProcオブジェクトはcallで呼び出しができる
# ブロックをProc.newでオブジェクト化している
proc = Proc.new { |food| p food }

proc.call("りんご") #=> "りんご"
# callで生成したProcオブジェクトを呼び出している
# 引数を渡すこともできる

ブロックを引数で明示的に受け取る方法

def method(&proc) # ブロックをprocオブジェクトに変換している
  proc.call
end

method { p 'とまと' } #=> "とまと"
  • メソッド呼び出し時にブロックを引数として渡している
  • 受け取り側のメソッドは&から始まるブロック引数を使用することで、ブロックを明示的に受け取ることができる
  • (&proc)では渡されたブロックがProcオブジェクトに変換されている

(Proc難しい)

lambda(ラムダ)

Procオブジェクトを作る方法の一つ。

lamd = lambda { |n| p n }

みたいな形でprocオブジェクトを生成できる

ちなみにギリシャ文字の第11文字であるΛ・λのことを言うらしい。

余談ですが、ラムダ技術部というYouTuberさんの名前の由来がよく分かりました。

procとlambdaは何が違うのか

引数チェック

  • lambdaの場合、渡す引数の数が違うとArgumentErrorになる
  • procの場合は、先頭から必要な数だけとって後は無視し、少ないと足りない部分にnilを割り当てる
proc = proc { |n| p n }
proc.call( 'proc', 'lambda' )

lamd = lambda { |n| p n }
lamd.call('lambda') #=> "lambda"
lamd.call('lambda', 'proc') #=> wrong number of arguments (given 2, expected 1)

returnの挙動

  • lambdaの場合、returnした後にメソッドに戻り、メソッドを最後まで実行する
  • Proc.newの場合はreturn後にメソッド自体を抜けてしまう
def lambda_method
  proc = Proc.new { return p "りんご" }
  proc.call
  p "ごりら"
end

# lambdaの場合、return後はメソッドに戻り最後まで実行するので"ごりら"も出力されている
method #=> "りんご"
              #=> "ごりら"

def proc_method
  proc = Proc.new { return p "わかめ"}
  proc.call
  p "たまねぎ"
end

# procはreturn後にメソッド自体を抜けてしまうので、出力されるのは"わかめ"のみ
proc_method #=> "わかめ"

まとめ

  • いずれもブロックを持ち運びに便利なprocオブジェクトにしたもの
  • Proc.new {ブロック}lambda {ブロック}でブロックをオブジェクト化できる(他にもやり方があるが省略)
  • 渡す引数が多い場合、Proc.newでは先頭から必要な分だけ取り後は無視され、足りない部分にはnilが当て嵌められる。lambdaでは引数の数が異なるとArgumentErrorになる
  • return後の挙動として、Procではreturn後にメソッドを抜け、lambdaではreturn後にメソッドに戻り処理を最後まで実行する

条件付きバリデーション、->(lambda記法)続き

validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  • 条件付きバリデーションはある条件下の時のみ検証を行う
  • ->(lambda記法)はProcの一種。Procオブジェクトの生成を短く書くことができる
unless: Proc.new { |a| a.password.blank? }
↓
unless: -> { |a| a.password.blank? }

★ lambda記法(->)を用いて { new_record? || changes[:crypted_password] }のブロックをProcオブジェクト化している。それをif:オプションに渡している。

  • 条件付きバリデーションの引数にはシンボル、Array、Procが使えるのでいずれか使用
  • Procオブジェクトを使うメリットとしてはその場で条件が書けること。シンボルの場合は個別のメソッドを指定するという形になる
  • ワンライナーで収まる条件を場合はProcオブジェクトを使うといい
class Order < ApplicationRecord
  # シンボル使用の例
  validates :card_number, presence: true, if: :paid_with_card?

  def paid_with_card?
    payment_type == "card"
  end
end

バリデーションの条件詳細

if: -> { new_recrod? || changes[:crypted_password] }

↑これはどういう意味なのか

new_record? インスタンスメソッド

Active Recordのnew_record?インスタンスメソッドを使うと、オブジェトが既にデータベース上にあるかどうかを確認できます。
Active Record バリデーション - Railsガイド

newメソッドで新しくオブジェクトを作成しただけでは、オブジェクトはDBに属していない。saveメソッドを呼ぶことで、オブジェクトは適切なDBのテーブルに保存される。

以下のコンソール結果で納得

irb> p = Person.new(name: "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>

irb> p.new_record?
=> true

irb> p.save
=> true

irb> p.new_record?
=> false

保存する前のデータである場合にnew_record?の結果がtrueになる。ニューレコードである。つまりオブジェクトが新規作成されたものであれば、検証を通す。

changes[:crypted_password]

if: -> { new_record? || changes[:crypted_password] }

ユーザーがパスワード以外のプロフィール項目を更新したい場合に、パスワードの入力を省略できるようになる。

irb(main):010:0> user.first_name = "太郎"
=> "太郎"
irb(main):011:0> user.changes
=> {"first_name"=>["たろう", "太郎"]}
irb(main):012:0> user.changes[:first_name]
=> ["たろう", "太郎"]
irb(main):013:0> user.changes[:crypted_password]
=> nil

色々調べてみたけど、どういう理屈でパスワードの入力が省略されているのか分からなかった。。
→パスワードの入力が省略されているというより、if: -> { new_record? || changes[:crypted_password] }によって、そのuserオブジェクトが新規作成されたものか、変更を加えようとしているのがuserオブジェクトのパスワードなのかを判定し、そうならば前述の検証にかけるという仕組み なのかな?と思った。

★ とりあえず、パスワード意外を編集したい場合にパスワード入力を省略できる記載なんだなと納得しておく。

validates :password, length: { minimum: 8 }, if: -> { new_record? || changes[:crypted_password] }
  • パスワードの入力は8文字以上
validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }
  • confirmation:バリデーションヘルパーの一つ
  • 二つのフィールドで受け取る内容が完全に一致する必要がある場合に使う
  • このヘルパーによって、確認したい属性(この場合password)に「_confirmation」を追加した仮想の属性を作成される

ビューでは下記のようなフィールドを用意

<%= f.text_field :user, :email %>
<%= f.text_field :user, :email_confirmation %>
  • このバリデーションチェックは、password_confirmationがnilでない場合のみ行われる
  • 確認を必須にするために、password_confirmation属性にpresence: trueを追加する
  • presence: trueは指定された属性が空でないこと(値がnilや空でもホワイトスペースでもないこと)を確認するため、内部でblank?メソッドを使っている
validates :email, uniqueness: true
  • これはemailが一意であり、重複していないことを検証する

3. コントローラ作成

ユーザー新規作成コントローラ

$ bundle exec rails g controller users

ルーティング

Rails.application.routes.draw do
  # root_pathで飛ぶページは適当に作っておく
  root 'home#top'

  # ログイン用のuser_sessionsコントローラ
  get 'login', to: 'user_sessions#new'
  post 'login', to: 'user_sessions#create'
  delete 'logout', to: 'user_sessions#destroy'

  # user新規作成機能のresources
  resources :users, only: %i[new create]
end
class UserController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to login_path # 新規作成成功後はログイン画面に
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation, :name)
  end
end

新規作成フォーム

<!-- 新規作成はユーザーをテーブルに登録する関係でモデルと紐づいているのでmodel:に@userを渡す-->
<%= form_with model: @user, local: true do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>

  <%= f.label :email %>
  <%= f.text_field :email %>

  <%= f.label :password %>
  <%= f.password_field :password %>

  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation %>

  <%= f.submit '登録' %>
<% end %>

ログイン用コントローラ

$ bundle exec rails g controller user_sessions
class UserSessionsController < ApplicationController
  def new; end

  def create
    @user = login(params[:email], params[:password])
    if @user
      redirect_back_or_to root_path, success: "ログインしました"
    else
      flash.new[:danger] = "ログインに失敗しました"
      render :new
  end

  def destroy
    logout
    redirect_to root_path, success: "ログアウトしました"
  end
end

ログインフォーム

<h1>ログイン</h1>
<%= form_with url: login_path, local: true do |f| %>
  <%= f.label :email %>
  <%= f.text_field :email, class: 'form-control' %>

  <%= f.label :password %>
  <%= f.password_field :password, class: 'form-control' %>
       
  <%= f.submit 'ログイン', class: 'btn btn-primary' %>
<% end %>
<%= link_to '登録ページへ', new_user_path %>
<a href="#">パスワードをお忘れの方はこちら</a>
  • ログインは新たに情報を登録するわけではないので、モデル(テーブル)と紐づいていない
  • このためurl:オプションでパスを指定するだけでいい
  • フォーム内容はHTTP POSTメソッドとして送信されるため、post 'login', to: 'user_sessions#create'のルートでcreateメソッドが実行されログインできる

form_withの話

modelオプション

  • 基本的にform_withのオプションはmodel:を使用する
  • 引数にはモデルクラスのインスタンス(保存したいテーブルのクラスのインスタンスを指定する
    usersテーブルに新たにレコードを作成したい場合、下記のように記述
def new
  @user = User.new
end
# 新規作成したレコードなのでcreateアクションが動く

def edit
  @user = User.find(params[:id])
end
# 既存のレコードを取得しているためupdateアクションが動く

↑ここで作成したインスタンスをフォームに渡す

<%= form_with model: @user %>
  • ここでモデル.newで新たに作成され、何も情報を持っていなければ自動的にcreateアクションへ
  • findメソッドなどで作成され、既に情報を持っている場合はupdateアクションへ振り分けられる
url:オプションを使う場合
  • モデルとフォームが紐づかない場合(例: ログインフォーム: user_sessions)
  • createやupdateを行うコントローラとモデルが紐付かない場合
    (model: @userと設定したが、別のコントローラに処理が遷移して欲しい場合)
  • @userのようなインスタンス変数を渡さずにurlオプションのみを設定した場合、入力ミスなどで編集処理が失敗した時はフォームへの入力値が一々消えてしまう。失敗しても元入力した値を残しておきたいのならmodelオプションにインスタンスを渡す

認証機能の補足

以下メソッドについて

  • requre_login
  • not_authenticated
  • redirect_back_or_to
not_authencticated
  • authenticatedは認証済みという意味。ということは認証済みでないユーザーに対して行われる処理

公式リファレンス

# The default action for denying non-authenticated users.
# You can override this method in your controllers,
# or provide a different method in the configuration.
def not_authenticated
  redirect_to root_path
end
  • 認証されていないユーザーを拒否する
  • コントローラーでこのメソッドを上書きできる
  • または構成で別の方法を提供している

→ 認証されていないユーザーをroot_pathに飛ばす?

redirect_back_or_toメソッド
# used when a user tries to access a page while logged out, is asked to login,
# and we want to return him back to the page he originally wanted.
def redirect_back_or_to(url, flash_hash = {})
  redirect_to(session[:return_to_url] || url, flash: flash_hash)
  session[:return_to_url] = nil
end
  • ユーザーがログインしていない状態であるページにアクセスした場合、まずログインするように促し、ログイン後はユーザーがアクセスしていたページに戻してあげるメソッド
require_login
# To be used as before_action.
# Will trigger auto-login attempts via the call to logged_in?
# If all attempts to auto-login fail, the failure callback will be called.
def require_login
  return if logged_in? # ログインユーザーがいるならメソッドを終了する

  # ここは解釈が合っているのか分からない
  # たぶんredirect_back_or_toのリクエストを保存する過程なのかなと・・・
  if Config.save_return_to_url && request.get? && !request.xhr? && !request.format.json?
    session[:return_to_url] = request.url
  end

  # 未ログインの場合以下の処理が実行され、結果not_authenticatedメソッドでリダイレクトが行われる
  send(Config.not_authenticated_action)
end
  • before_actionとして使用される
  • logged_inの呼び出しを介して自動ログイン
def logged_in?
  !!current_user
end
  • current_userが存在するのか存在しないのかtrue/falseを返す

!!←こいつは何なのか

結論:戻り値をtrueかfalseに揃える

!!」の動作: すべてをtruefalseにする
ご存じのとおり、Rubyではnilとfalse以外のオブジェクトはすべてtrueとして扱われます。そして否定の論理演算子「!」はtrueとfalseを反転します。

適当に試してみた

irb(main):008:0> !nil
=> true
irb(main):009:0> !!nil
=> false
irb(main):010:0> !!true
=> true
irb(main):011:0> !true
=> false
irb(main):012:0> !!false
=> false
irb(main):013:0> !false
=> true
irb(main):014:0> !!""
=> true
irb(main):015:0> !!"あいう"
=> true
irb(main):016:0> !""
=> false

感想

サクッと復習するつもりが意外と分かっていないことが分かってモリモリになってしまった。。最初期の頃よりかは公式文書への抵抗感が少なくなって良かった。

参考にしたサイト

Simple Password Authentication · Sorcery/sorcery Wiki · GitHub

sorceryによるログイン機能|tumu|note

【Rails】ログイン機能を実装 - Qiita