Railsチュートリアル12章まとめ。メールによるパスワード再設定実装

Railsチュートリアル 12章を終えました。
12章ではパスワード忘れた際の、メールでのパスワード再設定方法を見ていきます
11章のサインアップ時のメール認証とだいたい似た流れで実装できます

railstutorial.jp

概要

パスワード忘れた際のメールの設定方法については以下の流れで作っていきます

1.ユーザがログインページから"パスワード忘れた場合"のリンクを再設定した場合、ユーザーが送信したメールアドレスをキーにしてデータベースからユーザーを見つける
2.メールアドレスがデータベースにある場合は、再設定用トークンとそれに対応するリセットダイジェストを生成する
3.再設定用ダイジェストはデータベースに保存しておき、再設定用トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
4.ユーザーがメールのリンクをクリックしたら、メールアドレスをキーとしてユーザーを探し、データベース内に保存しておいた再設定用ダイジェストと比較する (トークンを認証する)
5.認証に成功したら、パスワード変更用のフォームをユーザーに表示する



要は、
パスワード忘れた場合のリンクをユーザが押下した⇨メールアドレスをDBに問い合わせる⇨
D再設定用ダイジェストをDBに保存し、それをメールに有効化リンクを作成⇨
メール内のリンクから、メアドをキーにしてDBを検索して再設定用のダイジェストと比較する⇨
認証してパスワード変更フォームを表示


アカウント認証をする際の違いは、パスワード変更フォームを表示する点です

パスワードリセット準備

パスワード再設定用のコントローラを作っていきます。ここではnewとeditアクションも作っていきます

$ rails generate controller PasswordResets new edit --no-test-framework

新しいパスワードの設定用モデル

次にリセット用のダイジェストを格納するreset_digest属性と、リセットした時間を記録するreset_sent_at属性をUserモデルに追加していきます。Userモデルは以下のようになります

Userモデルreset属性追加
Userモデルreset属性追加

ここでreset_digest属性についてトークンをセキュリティ上平文で保存しないためにハッシュ化したものを保存します
次にreset_sent_at属性は再設定用のリンクはなるべく短時間 (数時間以内) で期限切れになるように、リセットした時間を記録します

$ rails generate migration add_reset_to_users reset_digest:string \
> reset_sent_at:datetime
$ rails db:migrate

パスワードフォーム

パスワード設定フォーム

パスワードを再設定するための、メールアドレス入力フォームを作成していきます。

パスワード修正用email送信フォーム
パスワード修正用email送信フォーム

<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:password_reset, url: password_resets_path) do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.submit "Submit", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

createコントローラとモデル作成

次にフォームからsubmitを押下した時の動作をコントローラとモデルに書いていきます

app/controller/password_resets_controller.rb

  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "Email sent with password reset instructions"
      redirect_to root_url
    else
      flash.now[:danger] = "Email address not found"
      render 'new'
    end
  end

app/model/user.rb

  attr_accessor :remember_token, :activation_token, :reset_token

  # パスワード再設定の属性を設定する
  def create_reset_digest
    self.reset_token = User.new_token
    update_columns(reset_digest:  User.digest(reset_token), reset_sent_at: Time.zone.now)
  end

  # パスワード再設定のメールを送信する
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end

ここでコントローラではUserモデルからフォームで入力したemailを照合して
もし存在したらパスワード再設定用のダイジェストと時間をUserモデルに設定して
メールを送信します

メール文設定

次にメール文について設定していきます

app/mailers/user_mailer.rb

  def password_reset(user)
    @user = user
    mail to: user.email, subject: "Password reset"
  end

app/views/user_mailer/password_reset.text.erb

To reset your password click the link below:

<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>

This link will expire in two hours.

If you did not request your password to be reset, please ignore this email and
your password will stay as it is.

app/views/user_mailer/password_reset.html.erb

<h1>Password reset</h1>

<p>To reset your password click the link below:</p>

<%= link_to "Reset password", edit_password_reset_url(@user.reset_token,
                                                      email: @user.email) %>

<p>This link will expire in two hours.</p>

<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>

プレビュー

ここでメールプレビューを表示することもできます

test/mailers/previews/user_mailer_preview.rb

  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset
  def password_reset
    user = User.first
    user.reset_token = User.new_token
    UserMailer.password_reset(user)
  end

コメントのURLを叩くとメールのプレビューすることができます

edit設定

これでメールを送信することができるので、次に送信後メール内のリンクを押下した時の動きを設定していきます
リンクを押下することでパスワードリセット用フォームを表示します

app/views/password_resets/edit.html.erb

<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= hidden_field_tag :email, @user.email %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Update password", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

hidden_field_tagではparam[:email]にemailを格納します
もしf.hidden_fieldとするとparams[:user][:email]にemailを格納します。


editコントローラとモデル作成

コントローラとモデルを作成していきます

app/controllers/password_resets_controller.rb

class PasswordResetsController < ApplicationController
  before_action :get_user,   only: [:edit, :update]
  before_action :valid_user, only: [:edit, :update]


  def edit
  end

  private

    # 正しいユーザーかどうか確認する
    def valid_user
      unless (@user && @user.activated? &&
              @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
      end
    end

    # トークンが期限切れかどうか確認する
    def check_expiration
      if @user.password_reset_expired?
        flash[:danger] = "Password reset has expired."
        redirect_to new_password_reset_url
      end
    end

app/model/user.rb

  # パスワード再設定の期限が切れている場合はtrueを返す
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

モデルで reset_sent_at < 2.hours.agoとすることで、reset_sent_atに設定された時間が現時間より2時間前かどうかを確認し、
2時間より前の場合trueになります

パスワードを更新する

最後にパスワード更新された時の処理を作成します ここでの処理は下記のような手順で行います

1.パスワード再設定の有効期限が切れていないか 2.無効なパスワードであれば失敗させる (失敗した理由も表示する) 3.新しいパスワードが空文字列になっていないか (ユーザー情報の編集ではOKだった) 4.新しいパスワードが正しければ、更新する

1.パスワード再設定の有効期限が切れていないか
についてはbefore_action :check_expiration, only: [:edit, :update]とすればできそうです


2.無効なパスワードであれば失敗させる
3.新しいパスワードが空文字列になっていないか

if params[:user][:password].empty?  # (3) への対応
    else
      render 'edit'                                     # (2) への対応
    end
end

でいけそうです

4.新しいパスワードが正しければ、更新する

@user.update_attributes(user_params)          # (4) への対応

  private

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

とすればいい感じかと

まとめるとこんな感じですね

  def update
    if params[:user][:password].empty?                  # (3) への対応
      @user.errors.add(:password, :blank)
      render 'edit'
    elsif @user.update_attributes(user_params)          # (4) への対応
      log_in @user
      @user.update_attribute(:reset_digest, nil)
      flash[:success] = "Password has been reset."
      redirect_to @user
    else
      render 'edit'                                     # (2) への対応
    end
  end

  private

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

最後に

これでメールからパスワードのリセットを行えます。
アカウント認証と似ている部分が多いですね

参照

いつも記事を書くときにお世話になっています。ありがとうございます

www.masalog.site