PFメモ日記(4) sorceryの復習
deviseを使おうと思ったけど、せっかくなのでsorceryの復習をしたい。
※ 内容ごちゃごちゃしてかなり分かりづらいです。
目次
- 1. sorceryをインストール
- 生成されたマイグレーションファイルにその他欲しい属性と制約を追加
- crypted_passwordとsaltとは?
- 2. バリデーション(長い)
- 条件付きバリデーションとは?
- Procとlambda(ラムダ)とは?
- procとlambdaは何が違うのか
- 条件付きバリデーション、->(lambda記法)続き
- バリデーションの条件詳細
- 3. コントローラ作成
- form_withの話
- 認証機能の補足
- !!←こいつは何なのか
- 感想
- 参考にしたサイト
1. sorceryをインストール
gem 'sorcery'
$ bundle exec rails g sorcery:install create app/models/user.rb create db/migrate/XXXXXXXXX_sorcery_core.rb
- Userモデルとマイグレーションファイルが生成されている
生成されたマイグレーションファイルにその他欲しい属性と制約を追加
# 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に揃える
「
!!
」の動作: すべてをtrue
かfalse
にする
ご存じのとおり、Rubyではnilとfalse以外のオブジェクトはすべてtrueとして扱われます。そして否定の論理演算子「!」はtrueとfalseを反転します。
- !nil #=> true
- !!nil #=> false
- !false #=> true
- !!false #=> false
- !!その他何でも #=> true 反転の反転は論理上何も変化をもたらしませんが、表現がtrueかfalseに揃えられるところがポイントです。
Rubyの否定演算子2つ重ね「!!」(double-bang)でtrue/falseを返す|TechRacho by BPS株式会社
適当に試してみた
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