はじめに
sorceryのReset Passwordモジュールを使用して、パスワードのリセット機能を実装していきましょう!
開発環境ではletter_opener_webを使用します。
基本的な実装方法は公式wikiを参考にしましょう。
作業の流れ
- sorceryのReset passwordモジュールをインストールする
- メイラーの作成
- コントローラを作成
- ビューの作成
- letter_opener_webを追加
- configの導入
sorceryのReset passwordモジュールをインストールする
手始めにsorceryのReset passwordモジュールをインストールするためのコマンドを入力します。
$ rails g sorcery:install reset_password --only-submodules
生成されたファイルを見ます。
> rails g sorcery:install reset_password --only-submodules
Running via Spring preloader in process 71125
gsub config/initializers/sorcery.rb
insert app/models/user.rb
create db/migrate/20211009040003_sorcery_reset_password.rb
config/initializers/sorcery.rb を確認
Rails.application.config.sorcery.submodules = [:reset_password]
生成されたマイグレーションファイルを確認
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_token
に関するカラムがいくつか追記されています。
確認できたらマイグレーションしましょう。
$ rails db:migrate
メイラーの作成
UserMailer
という名前でパスワードリセット用のメイラーを作成しましょう
$ rails g mailer UserMailer reset_password_email
生成されたファイルを見ます
> rails g mailer UserMailer reset_password_email
Running via Spring preloader in process 71280
create app/mailers/user_mailer.rb
invoke erb
create app/views/user_mailer
create app/views/user_mailer/reset_password_email.text.erb
create app/views/user_mailer/reset_password_email.html.erb
/mailers/user_mailer.rb
が生成されており、
user_mailer
の下に2つのビューファイルが生成されていますね。こちらは実際にユーザに対して送るメールの中身となるファイルです。テキスト形式とHTML形式になります。ユーザによってはHTML形式のメールを受け取りたくない人もいるので、テキストメールも作成しておきます。
パスワードリセットに使用するメイラーの定義
では次に、config/initializers/sorcery.rb
にreset_passwordサブモジュールを追加し、パスワードリセットに使用するActionMailerとしてUserMailerを定義する記述をしましょう。
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
end
end
このファイルは非常に長いファイルですが、よく見てみると必要な記述がコメントアウトされている中で既に揃っています。必要な記述を探してコメントアウトを外して使用しましょう。
今回必要な記述は、395行目の # user.reset_password_mailer =
の部分にUserMailer
を付け加えます。
メイラービューの作成
先程生成された2つのメイラービューファイルの中身を作成しましょう。
<%= @user.decorate.full_name %>様
===========================================
パスワード再発行のご依頼を受け付けました。
こちらのリンクからパスワードの再発行を行ってください。
<%= @url %>
<%= @user.decorate.full_name %>様
<p>パスワード再発行のご依頼を受け付けました。</p>
<p>以下のリンクからパスワードの再発行を行ってください。</p>
<p><a href="<%= @url %>"><%= @url %></a></p>
上記の@url
の中身は、tokenで発行された再登録用のurlが入っています。
メールを送信するメソッドを記述
作成したメールを送信するためのメソッドを記述します。
class UserMailer < ApplicationMailer
default from: 'from@example.com'
def reset_password_email
@user = User.find(user.id)
@url = edit_password_reset_url(@user.reset_password_token)
mail(to: user.email, subject: 'パスワードリセット')
end
end
default from: 'from@example.com'
でメールの送信元のアドレスを指定できるmail(to: user.email,subject: 'パスワードリセット')
でメールの宛先、件名を指定
こちらの@user
、@url
はどこで使われているのかというと、先ほど作成したメイラービューの中で使われています。
コントローラを作成
パスワードリセットを処理するコントローラーが必要なのでジェネレートコマンドで生成します。
$ rails g controller PasswordResets new create edit update
> rails g controller PasswordResets new create edit update
Running via Spring preloader in process 71542
create app/controllers/password_resets_controller.rb
invoke erb
create app/views/password_resets
create app/views/password_resets/new.html.erb
create app/views/password_resets/create.html.erb
create app/views/password_resets/edit.html.erb
create app/views/password_resets/update.html.erb
invoke decorator
create app/decorators/password_reset_decorator.rb
生成されたPasswordResetsコントローラの中身を記述する
公式wikiを参考にし、記述しましょう。
class PasswordResetsController < ApplicationController
# Rails 5以降では、次の場合にエラーが発生します。
# before_action :require_login
# ApplicationControllerで宣言されていません。
skip_before_action :require_login
# パスワードリセット申請画面へレンダリングするアクション
def new; end
# パスワードのリセットを要求するアクション。
# ユーザーがパスワードのリセットフォームにメールアドレスを入力して送信すると、このアクションが実行される。
def create
@user = User.find_by(email: params[:email])
# この行は、パスワード(ランダムトークンを含むURL)をリセットする方法を説明した電子メールをユーザーに送信します
@user&.deliver_reset_password_instructions!
# 上記は@user.deliver_reset_password_instructions! if @user と同じ
# 電子メールが見つかったかどうかに関係なく、ユーザーの指示が送信されたことをユーザーに伝えます。
# これは、システムに存在する電子メールに関する情報を攻撃者に漏らさないためです。
redirect_to login_path, success: t('.success')
end
# パスワードのリセットフォーム画面へ遷移するアクション
def edit
@token = params[:id]
@user = User.load_from_reset_password_token(params[:id])
return not_authenticated if @user.blank?
end
# ユーザーがパスワードのリセットフォームを送信したときに発生
def update
@token = params[:id]
@user = User.load_from_reset_password_token(params[:id])
return not_authenticated if @user.blank?
# 次の行は、パスワード確認の検証を機能させます
@user.password_confirmation = params[:user][:password_confirmation]
# 次の行は一時トークンをクリアし、パスワードを更新します
if @user.change_password(params[:user][:password])
redirect_to login_path, success: t('.success')
else
flash.now[:danger] = t('.fail')
render :edit
end
end
end
パスワードリセット申請時にはログインページにリダイレクトされるようにしましょう。
コードを紐解く
deliver_reset_password_instructions!メソッド
有効期限付きのリセットコード(トークン)を生成し、ユーザーに電子メールを送信します。
# Generates a reset code with expiration and sends an email to the user.
def deliver_reset_password_instructions!
mail = false
config = sorcery_config
# hammering protection
return false if config.reset_password_time_between_emails.present? && send(config.reset_password_email_sent_at_attribute_name) && send(config.reset_password_email_sent_at_attribute_name) > config.reset_password_time_between_emails.seconds.ago.utc
self.class.sorcery_adapter.transaction do
generate_reset_password_token!
mail = send_reset_password_email! unless config.reset_password_mailer_disabled
end
mail
end
また、@user&.deliver_reset_password_instructions!
ですが、&.
はメソッドの実行対象のオブジェクトがnil
(ユーザーがいなかったとき)だった場合を考慮した記法です。
@user = User.find_by(email: params[:email])
でユーザーが見つからなかったときは@user
にはnil
が入ります。そのnil
に対してdeliver_reset_password_instructions!
メソッドを実行すると、UndefinedMethod
の例外が発生します。
そこで&.
を使用すれば、もしnil
であってもnil
を返すだけになり、例外は発生しません。
load_from_reset_password_tokenメソッド
トークンでユーザーを検索し、有効期限もチェックします。 トークンが見つかり、有効な場合、ユーザーを返します。
# Find user by token, also checks for expiration.
# Returns the user if token found and is valid.
def load_from_reset_password_token(token, &block)
load_from_token(
token,
@sorcery_config.reset_password_token_attribute_name,
@sorcery_config.reset_password_token_expires_at_attribute_name,
&block
)
end
また、User.load_from_reset_password_token(params[:id])
にて、有効期限切れなどでユーザーが取得できなかったとき、not_authenticatedとなります。
change_passwordメソッド
トークンをクリアし、ユーザーの新しいパスワードを更新しようとします。
# Clears token and tries to update the new password for the user.
def change_password(new_password, raise_on_failure: false)
clear_reset_password_token
send(:"#{sorcery_config.password_attribute_name}=", new_password)
sorcery_adapter.save raise_on_failure: raise_on_failure
end
PasswordResetsコントローラのルーティングを設定
resources :password_resets, only: %i[new create edit update]
トークンにユニーク制約を付与
Userモデルにバリデーションを追記します。
tokenは一意なものでなければいけませんので、uniqueness: true
を付与します。
しかし、パスワードを変更した際、reset_password_token
はnilになるのでユニーク制約に引っかかってしまいます。nilを許可する必要があるでしょう。
そこでallow_nil
オプションを使います。allow_nil: true
を付与することで、対象の値がnilの場合にバリデーションをスキップします。
validates :reset_password_token, presence: true, uniqueness: true, allow_nil: true
ビューファイルの作成
パスワードリセット申請画面
<%= content_for(:title, t('.title')) %>
<div class="container">
<div class="row">
<div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
<h1><%= t '.title' %></h1>
<%= form_with url: password_resets_path, local: true do |f| %>
<div class="form-group">
<%= f.label :email, User.human_attribute_name(:email) %><br />
<%= f.email_field :email, class: 'form-control' %>
</div>
<%= f.submit t('password_resets.new.submit'),class: 'btn btn-primary' %>
<% end %>
</div>
</div>
</div>
こちらのform_with
はモデルと紐付いていない点に注意しましょう。また、そのことよりラベルが日本語化しませんので、自分で日本語化する記述を追加しましょう。
パスワードリセット画面
<%= content_for(:title, t('.title')) %>
<div class="container">
<div class="row">
<div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
<h1><%= t '.title' %></h1>
<%= form_with model: @user, url: password_reset_path(@token), local: true do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="form-group">
<%= f.label :email %>
<%= @user.email %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
</div>
<div class="actions">
<p class="text-center">
<%= f.submit class: 'btn btn-primary' %>
</p>
</div>
<% end %>
</div>
</div>
</div>
パスワードリセット申請画面リンクの表示
ログイン画面にパスワードリセット申請画面へのリンクを追加しましょう。
<% end %>
<div class='text-center'>
<%= link_to (t '.to_register_page'), new_user_path %>
<%= link_to t('.password_forget'), new_password_reset_path %>
</div>
</div>
</div>
letter_opener_webを追加
letter_opener_web
を使って開発環境では実際のメールは送られないように設定しましょう。
Gemfileにletter_opener_webを追加する
まず、gemを開発環境に追加し、bundleコマンドを実行してインストールします。
group :development do
gem 'letter_opener_web', '~> 1.0'
end
$ bundle install
ルーティングに追記
LetterOpenerWebにアクセスするために必要な記述を追記しましょう
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
config.action_mailer.delivery_method = :letter_opener_web
config.action_mailer.default_url_options = { host: 'localhost:3000' }
メールが送られたか確認する
こちらのURLにアクセスしてメールが送られてきているか確認してみましょう。
http://localhost:3000/letter_opener
configの導入
Configは環境固有の設定を簡単で使いやすい方法で管理できるようになるgemです。
host情報はconfigというgemを使ってsettings/development.yml
に記載する
まずGemfile
に追記しましょう。
gem 'config'
$ bundle install
config
を導入しましたら、こちらのコマンドを入力しましょう。
$ rails g config:install
> rails g config:install ?21_password_reset
Running via Spring preloader in process 78325
create config/initializers/config.rb
create config/settings.yml
create config/settings.local.yml
create config/settings
create config/settings/development.yml
create config/settings/production.yml
create config/settings/test.yml
append .gitignore
これにより、カスタマイズ可能な構成ファイルconfig/initializers/config.rb
と一連のデフォルト設定ファイルが生成されます。
config/initializers/config.rb | configの設定ファイル |
config/settings.yml | すべての環境で利用する定数を定義 |
config/settings.local.yml | ローカル環境のみで利用する定数を定義 |
config/settings/development.yml | 開発環境のみで利用する定数を定義 |
config/settings/production.yml | 本番環境のみで利用する定数を定義 |
config/settings/test.yml | テスト環境のみで利用する定数を定義 |
host情報をsettings/development.yml
に記載しましょう。
default_url_options:
host: 'localhost:3000'
config.action_mailer.perform_caching = false
config.action_mailer.default_url_options = Settings.default_url_options.to_h
config.action_mailer.delivery_method = :letter_opener_web
本番環境では以下のようにします。host: 'example.com'
の部分には自分のドメインを設定します。
default_url_options:
protcol: 'https'
host: 'example.com'
config.action_mailer.default_url_options = Settings.default_url_options.to_h
終わりに
公式wikiの通りに行うことである程度実装することができますが、しっかり手順を把握しておきましょう。
参考記事


コメント