はじめに
sorceryのパスワードリセット機能の実装を行いました。
以下3点について記述しましたので、ご興味あれば見てってください。
1. sorceryのパスワードリセット機能の実装方法
2. ActionMailerの使い方
3. passwordリセットトークンがリクエスト/レスポンス間でどのように使われているか
1. sorceryのreset_password
をインストール
モジュールの実装とマイグレーションファイルの作成
-
rails g sorcery:install reset_password --only-submodules
を実行し、password reset用のモジュールのインストール、マイグレーションファイルを作成する。
$ rails g sorcery:install reset_password --only-submodules
Running via Spring preloader in process 99290
gsub config/initializers/sorcery.rb
insert app/models/user.rb
create db/migrate/20200419075054_sorcery_reset_password.rb
- sorceryの定義ファイル
config/initializers/sorcery.rb
に、password_resetサブモジュールを使用するための記述が自動で行われている。
Rails.application.config.sorcery.submodules = [:reset_password]
Userモデルにallow_nil: trueを追加
- password_reset_token にユニーク制約と
allow_nil: true
を付与しておく。- パスワードを変更した際、reset_password_tokenがnilになるのでユニーク制約に引っかかってしまう。そこで、allow_nil:trueを加えることでnilを許可しておく。
validates :reset_password_token, uniqueness: true, allow_nil: true
2. パスワードリセット用のMailerを作成
application_mailer.rb
の設定
メールの差し出し元とMailerビューの共通レイアウトとなるファイルを指定。
- ここではメールの差出元をadmin@example.comとしておく。
- レイアウトファイルは
app/views/layouts/mailer.html.erb
とapp/views/layouts/mailer.text.erb
になる。
class ApplicationMailer < ActionMailer::Base
# メールの差し出し元
default from: 'admin@example.com'
layout 'mailer'
end
パスワードリセットに使用するメイラーの設定
-
rails g mailer UserMailer change_password_email
を実行し、パスワードリセットに使用するメイラーUserMailerを作成。 -
config/initializers/sorcery.rb
の中で、sorceryのパスワードリセットに使用するActionMailerとして、UserMailerを指定する。 -
user.reset_password_mailer = UserMailer
のUserMailerから、app/mailers/user_mailer.rb
が推測される。
Rails.application.config.sorcery.submodules = [:reset_password, blabla, blablu, ...]
Rails.application.config.sorcery.configure do |config|
config.user_config do |user|
user.reset_password_mailer = UserMailer # パスワードリセット用のMailerにUserMailerが指定されている
end
end
パスワードリセット用のメソッドを記述
- メーラー(user_mailer.rb)を編集し、パスワードリセット用メールを送信するためのメソッドを作成し、引数に
user
パラメータを追加する。 - メール(view)内に表示させる情報やメールの送信先を設定。
-
Mailerクラス#メソッドはコントローラ#アクションと似た動きをする。
→UserMailer
クラスのchange_password_email
メソッドから、user_mailer/change_password_email.html.erb
というメイラービューを推測し、それを呼び出す。
class UserMailer < ApplicationMailer
# change_password_emailメソッドなので、change_password_email.〇〇のビューがメールのフォーマットになる
# コントローラの場合と同様、メイラーのメソッド内で定義されたインスタンス変数はメイラーのビューで使える。
def change_password_email(user)
@user = User.find(user.id)
@url = edit_password_change_url(@user.reset_password_token)
mail(to: user.email,
subject: 'パスワードリセット')
end
end
メイラービューの設定
これが実際のメール本文となる。
<%= @user.name %>様
<p>以下のリンクからパスワードの再発行を行ってください。</p>
<p><a href="<%= @url %>"><%= @url %></a></p>
<%= @user.name %>様
===========================================
以下のリンクからパスワードの再発行を行ってください。
<%= @url %>
3. コントローラの設定
パスワードリセットを行うためのコントローラ#アクションを、rails g controller PasswordChanges new create edit update
で追加。
class PasswordChangesController < ApplicationController
skip_before_action :require_login
# パスワードリセット申請フォーム用のアクション
def new; end
# パスワードリセットをリクエストするアクション
# ユーザーがパスワードのリセットフォームにemailを入力し、送信したときにこのアクションが実行される
def create
# form_withで送られてきたemailをparamsで受け取る
@user = User.find_by(email: params[:email])
# DBからデータを受け取れていれば、パスワードリセットの方法を記載したメールをユーザーに送信する(ランダムトークン付きのURL/有効期限付き)
@user&.deliver_reset_password_instructions!
# 上記は、@user.deliver_reset_password_instructions! if @user と同じ
# フォームに入力したemailがアプリ(DB)内に存在するか否かを問わず、リダイレクトして成功メッセージを表示させる。
# DBに存在した時だけ成功メッセージを表示させると、DB内にそのemailが存在するかどうかを悪意ある第三者でさえも確認できてしまう。
redirect_to login_path, success: "成功しました"
end
# パスワードリセットフォームページへ遷移するアクション
def edit
# postされてきた値を取得
@token = params[:id]
# リクエストで送信されてきたトークンを使って、ユーザーの検索を行い, 有効期限のチェックも行う。
# トークンが見つかり、有効であればそのユーザーオブジェクトを@userに格納する
@user = User.load_from_reset_password_token(params[:id])
# @userがnilまたは空の場合、not_authenticatedメソッドを実行する
return not_authenticated if @user.blank?
end
# ユーザーがパスワードのリセットフォームを送信(新しいパスワードの入力)したときに実行される
def update
@token = params[:id]
@user = User.load_from_reset_password_token(@token)
return not_authenticated if @user.blank?
# password_confirmation属性の有効性を確認
@user.password_confirmation = params[:user][:password_confirmation]
# change_passwordメソッドで、パスワードリセットに使用したトークンを削除し、パスワードを更新する
if @user.change_password(params[:user][:password])
redirect_to login_path, success: "成功しました"
else
flash.now[:danger] = "失敗しました"
render :edit
end
end
end
4. パスワードリセット用のフォーム作成
パスワードリセット申請フォーム
<%= form_with url: password_changes_path, local: true do |f| %>
<%= f.label :email, "メールアドレス" %>
<%= f.email_field :email %>
<%= f.submit "送信する" %>
<% end %>
パスワードリセット用のフォーム
<%= form_with model: @user, url: password_changes_path(@token), local: true do |f| %>
<%= f.label :email %><br>
<%= @user.email %>
<%= f.label :password %><br>
<%= f.password_field :password %>
<%= f.label :password_confirmation %><br>
<%= f.password_field :password_confirmation %>
<%= f.submit %>
<% end %>
5. トークンはどのように動いて、どのように使用されているのか
パスワードリセットの一連の流れの中で、どのようにトークンが使用されているのか?
1. createアクション実行時のログを確認
- 新しく発行されたトークンreset_password_token
- パスワードリセット用メールが送信された日時change_password_email_sent_at
が新しく更新されている。
User Update All (4.9ms) UPDATE "users" SET "reset_password_token" = 'h1mJ......2i',
"change_password_email_sent_at" = '2020-04-29 17:09:12.464274' WHERE "users"."id" = ? [["id", 43]]
2. アプリケーションから送信されてきたメールを確認
URLにreset_password_tokenが埋め込まれている事が分かる。(h1mj~2iの部分)
3. editアクションでデバッグしてみる
-
idにreset_password_tokenのデータが格納されている。
→params[:id]
、User.load_from_reset_password_token(params[:id])
のparams[:id]でトークンを取得し、そのトークンが、DBに保存されているトークンと合致するかを確認する。(updateアクションも同様)
12: def edit
13:
14: binding.pry
15:
=> 16: @token = params[:id]
17: @user = User.load_from_reset_password_token(params[:id])
18: return not_authenticated if @user.blank?
19: end
[1] pry(#<PasswordChangesController>)> params
=> <ActionController::Parameters {"controller"=>"password_changes",
"action"=>"edit", "id"=>"h1mJZ.....2i"} permitted: false>