Railsチュートリアル 12章を終えました。
12章ではパスワード忘れた際の、メールでのパスワード再設定方法を見ていきます
11章のサインアップ時のメール認証とだいたい似た流れで実装できます
概要
パスワード忘れた際のメールの設定方法については以下の流れで作っていきます
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モデルは以下のようになります
ここでreset_digest属性についてトークンをセキュリティ上平文で保存しないためにハッシュ化したものを保存します
次にreset_sent_at属性は再設定用のリンクはなるべく短時間 (数時間以内) で期限切れになるように、リセットした時間を記録します
$ rails generate migration add_reset_to_users reset_digest:string \ > reset_sent_at:datetime $ rails db:migrate
パスワードフォーム
パスワード設定フォーム
パスワードを再設定するための、メールアドレス入力フォームを作成していきます。
<% 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
最後に
これでメールからパスワードのリセットを行えます。
アカウント認証と似ている部分が多いですね
参照
いつも記事を書くときにお世話になっています。ありがとうございます