見出し画像

Sorceryでパスワードリセット機能を実装する。

こんにちは、わったんです。今回はsorceryでパスワードリセット機能を実装したので、まとめていきます。


モジュール(reset_password)の導入

sorceryにパスワードリセット機能を簡単につけられるモジュールが用意されているので、このモジュールを今回は用います。基本は下のGithubの説明に従っていけば実装できる流れになっています。

rails g sorcery:install reset_password --only-submodules

まずはターミナルに上のようにコードを打ってモジュールのインストールを行います。すると下のようなマイグレーションファイルが作られます。

class SorceryResetPassword < ActiveRecord::Migration[5.2]
 def change
   add_column :users, :reset_password_token, :string, default: nil
   add_column :users, :reset_password_token_expires_at, :datetime, default: nil
   add_column :users, :reset_password_email_sent_at, :datetime, default: nil
   add_column :users, :access_count_to_reset_password_page, :integer, default: 0

   add_index :users, :reset_password_token
 end
end

[reset_password_email_sent_atカラム]はパスワードリセット用のメールを送信した時刻を保持するためのカラムで[reset_password_token_expires_atカラム]はトークンの有効期限が切れる時刻を記録するカラムです。この二つのカラムを使って、トークンの有効期限を設定することができるのではないかと思われます。[reset_password_token]がトークン文字列を保持するカラムとなります。
[access_count_to_reset_password_pageカラム]はパスワードリセット画面にアクセスした回数を記録するために用意されたカラムです。N回以上はパスワードリセット画面に移動できないようにするなどの機能を実装する時に用いられるカラムです。今回に関しては、このカラムは使用しません。

bundle exec rails db:migrate

これでマイグレートさせます。下のようにuserテーブルにカラムが追加されました。

schema.rb

<省略>

  create_table "users", force: :cascade do |t|
   t.string "email", null: false
   t.string "crypted_password"
   t.string "salt"
   t.string "last_name", null: false
   t.string "first_name", null: false
   t.datetime "created_at", null: false
   t.datetime "updated_at", null: false
   t.string "Avatar_image"
   t.string "reset_password_token"
   t.datetime "reset_password_token_expires_at"
   t.datetime "reset_password_email_sent_at"
   t.integer "access_count_to_reset_password_page", default: 0
   t.index ["email"], name: "index_users_on_email", unique: true
   t.index ["reset_password_token"], name: "index_users_on_reset_password_token"
 end
 
 <省略>

そもそもトークンとは何かについてですが、イメージとしてはこちらのサイトの画像のような感じだと思います。ある一定の規則に基づいて文字列を作成するが、その規則性が予測しずらいものがトークンであるとイメージしています(例えばある文字列がログインに必要であり、その文字列は発行されてから1時間しかログインのために使用できず、1時間を超えると新たに発行された文字列を使用しないとログインできないとします。この場合、ある時間帯ではABCDの文字列が発行され、それから1時間経つとEFGHの文字列が発行されるとする状況だと、1時間ごとに文字列が4つ後ろにずれていく規則性が簡単にわかってしまうので、今の時間帯とある時間帯における文字列が分かれば簡単に文字列の内容が予想でき、外部の者も簡単になりすましてログインできるようになってしまいます。しかしある時間帯でKJFDが発行され、それから1時間後にIJRHが発行されるような場合、規則性がわからないため時間と文字列の規則性が外部からわかりづらくなります。このようなものがトークンであり、仮に外部からある時間帯に発行された文字列が見られても、1時間後、2時間後に発行される文字列が予想できないため、外部の者がなりすましてログインすることを困難にすることができます。)。私なりの解釈ですので「ちょっと違うよ!」って思われた方は教えて頂ければと思います。

次は新しく追加されたカラムに対して下のようにバリデーションを設定します。

user.rb

class User < ApplicationRecord
<省略>

validates :reset_password_token, uniqueness: true, allow_nil: true

<省略>

トークンの文字列が重複しないようにユニーク制約をかけます。しかしallow_nilオプションでnilの重複については許容できるようにします。ユーザー登録初期はこのカラムはnilなので、複数人がユーザー登録してしまうとnilが重複してしまい、ユーザー登録ができない事態になります。これを避けるためallow_nilオプションを有効にします。

Mailerの作成

パスワードをリセットする時に自分宛にURL付きのメールが送られて、そのURL先に飛んで新しいパスワードを設定した経験があると思います。このようにパスワードの更新を行う時には、本人宛のメールを送信する作業を挟むことでなりすましを行いにくくできると思われるので、今回の機能でもパスワードリセット時にパスワードリセット用のURLを貼り付けたメールを送信する機能をつけます。

まずはMailerの作成を行います。

rails g mailer UserMailer reset_password_email

次はsorceryにreset_passwordのサブモジュールを追加し、使用するMailerの設定を行います。

config/initializers/sorcery.rb
 #reset_passwordサブモジュールを追加する 
Rails.application.config.sorcery.submodules = [:reset_password]

Rails.application.config.sorcery.configure do |config|
   config.user_config do |user|
    #mailerの設定を行う 
    user.reset_password_mailer = UserMailer
   end
   
<省略>

Mailerの設定を行います。Mailerはコントローラと役目が似ていて、テンプレートを通じてメールの作成・送信を行います。

app/mailers/user_mailer.rb

class UserMailer < ApplicationMailer

 #reset_password_emailメソッドは引数としてuserオブジェクトを受け取る
 def reset_password_email(user)
   @user = User.find(user.id)
   #このurlでリダイレクトするとパスワードがリセットされる。
   @url = edit_password_reset_url(@user.reset_password_token)
   #toは宛先、subjectは件名を指定する。
   mail(:to => user.email,
       :subject => "Your password has been reset")
 end
end

コントローラー側でメールを送信する処理が実行されると、このメーラーが働くのだと思われます。今回の場合は、reset_password_emailメソッドが引数としてuserオブジェクトを受け取り、メールテンプレートにユーザー情報やパスワードリセット用のURL、メールの宛先や件名の情報を渡しています。

最後にメールテンプレート(メールのレイアウト)の作成です。html形式とtext形式の2パターンを設定できます。今回はhtml形式のテンプレートを載せておきます。full_nameメソッドはユーザーの性と名を表示するメソッドであり、decorateで独自に定義したものになります。

views/user_mailers/reset_password_email.html.erb

<p><%= @user.decorate.full_name %>様</p>
<p>==============================================</p>
<p>パスワード再発行のご依頼を受け付けました。</p>
<p>以下のリンクからパスワードの再発行を行なってください</p>
<p><a href="<%= @url %>"><%= @url %></a></p>

ルーティングの設定

password_resetsのルーティングを設定します。追加するアクションはcreate, edit, update, newの4つになります。

routes.rb

Rails.application.routes.draw do

<省略>

  resources :password_resets, only: %i[create edit update new]
 
<省略>

コントローラーの作成

次はコントローラーの作成になります。以下のようにアクションを追加します。

bundle exec rails g controller PasswordResets create edit update new

コントローラーは以下のように書いていきます。パスワードリセット時の大まかな流れとしては、①パスワードリセットの申請画面に飛ぶ(newアクション)→②パスワードリセット申請画面のフォームにユーザーのメールアドレスを記入してフォームを送るとcreateアクションが働き、トークンが発行されてトークンの文字列がユーザーテーブルに保存され、さらにparamsにトークンの文字列を含んだURLが貼り付けられたメールが送信される→③送られたメールのリンクからリダイレクトすると、editアクションが働き新しいパスワードを入力する画面が表示される→④フォームにパスワードを入力して更新すると、updateアクションが働きパスワードが変更される。以上になります。細かい点は異なっているかもしれませんが、大体の流れはこのような感じだと思います。

class PasswordResetsController < ApplicationController
 skip_before_action :require_login, only: %i[create edit update new]
 
  #パスワードリセット申請フォーム用のアクション  def new; end
 
 #トークンを発行し、ユーザーにパスワードリセット用のリンク付きメールを送信するアクション
 def create
   #記入したemailからユーザーを探す。
   @user = User.find_by(email: params[:email])
   #ユーザーのemailにインストラクションを送るコード
   #deliver_reset_password_instruction!→有効期限付きのリセットコードを作成し、ユーザーにメールを送信するメソッド。
   @user.deliver_reset_password_instructions! if @user
   redirect_to login_path, success: 'Instructions have sent to your email'
 end

  #新しいパスワードを入力する画面に移動するときに働くアクション  def edit
   @token = params[:id]
   #load_from_reset_password_token→モデルからトークンを探し、トークンが見つかり且つ有効であればユーザーを返す。
   @user = User.load_from_reset_password_token(params[:id])

   if @user.blank?
     not_authenticated
     return
   end
 end

 #パスワードをリセットし、再登録を実行するアクション
 def update
   @token = params[:id]
   @user = User.load_from_reset_password_token(@token)
   if @user.blank?
     not_authenticated
   end
   #password_confirmation属性の有効性を確認
   @user.password_confirmation = params[:user][:password_confirmation]
   #トークンをクリアして、ユーザーの新しいパスワードを更新しようとする。
   if @user.change_password(params[:user][:password])
     redirect_to login_path, success: 'パスワードを変更しました。'
   else
     flash.now[:danger] = 'パスワードの変更に失敗しました。'
     render :edit
   end
 end
end

ではさらに具体的にそれぞれのメソッドについて説明していきます。

createアクションは先ほど説明したように、トークンの発行•保存とユーザーにパスワードリセット用のURLをメールとして送信する働きを持っています。deliver_reset_password_instructions!メソッドがトークンをDBに保存して、トークンの配列が含まれるURLを保持するメールを送信する処理を行います。@user.deliver_reset_password_instructions! if @userのようにif文を用いることでエラーを起こさないようにしています。もしエラーが起こってしまうと、存在するメールアドレスと存在しないメールアドレスが第三者に漏洩する危険性があるため、存在しないメールアドレスだったとしても、とりあえずフラッシュメッセージを送るようにしています。

editアクションは新しいパスワードを入力するためのフォームの表示に関わるメソッドです。URLからトークンの配列を取得し、トークンの情報をもとにユーザーの照合を行います。もしトークンの情報からユーザーが見つからなければ、not_authenticatedメソッドで設定しているリダイレクト先に飛びます。

updateアクションは既存のパスワードをリセットし、フォームに登録されたパスワードを保存するメソッドになります。change_passwordメソッドで既存パスワードの削除と新しいパスワードの保存を行います。

これらのメソッドに関してはこちらのサイトを参考にすると良いと思います。

最後にパスワードリセット用のリンクをビューに記載して終了です。

views/user_sessions/new.html.erb

<%= link_to t('.password_forget'), new_password_reset_path %>


letter_opener_webを使用する

メール送信機能を搭載するときに、開発環境で実際にメールの送信が行えているかなどのテストを行いたいと思います。その時にこのletter_opner_webのgemが大変便利となります。実装の流れは下のGithubを参考にして頂ければと思います。

Gemfile

group :development do
 gem 'letter_opener_web'
end

Gemfileにgemを追加し、bundle installでgemのインストールを行います。

action_mailer.delivery_methodにletter_opener_webを追加します。

config/environments/development.rb

 config.action_mailer.raise_delivery_errors = false
 config.action_mailer.delivery_method = :letter_opener_web
 config.action_mailer.perform_deliveries = true
 config.action_mailer.default_url_options = { host: 'localhost:3000' }
 config.action_mailer.perform_caching = false

最後にルーティングの追加を行います。

routes.rb

Rails.application.routes.draw do
 if Rails.env.development?
   mount LetterOpenerWeb::Engine, at: '/letter_opener'
 end
 
<省略>

これでlocalhost:3000/letter_openerでletter_opnerにアクセスし、開発環境で送信されたメールの確認ができるようになりました。

不十分の点もまだまだあるとは思いますが、以上になります。最後まで見て頂き、ありがとうございました。

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