Ruby on Rails チュートリアル
-
第3版 目次
- 第1章 ゼロからデプロイまで
- 第2章 Toyアプリケーション
- 第3章 ほぼ静的なページの作成
- 第4章 Rails風味のRuby
- 第5章 レイアウトを作成する
- 第6章 ユーザーのモデルを作成する
- 第7章 ユーザー登録
- 第8章 ログイン、ログアウト
- 第9章 ユーザーの更新・表示・削除
- 第10章 アカウント有効化とパスワード再設定
- 第11章 ユーザーのマイクロポスト
- 第12章 ユーザーをフォローする
|
||
第3版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第3版 目次
- 第1章 ゼロからデプロイまで
- 第2章 Toyアプリケーション
- 第3章 ほぼ静的なページの作成
- 第4章 Rails風味のRuby
- 第5章 レイアウトを作成する
- 第6章 ユーザーのモデルを作成する
- 第7章 ユーザー登録
- 第8章 ログイン、ログアウト
- 第9章 ユーザーの更新・表示・削除
- 第10章 アカウント有効化とパスワード再設定
- 第11章 ユーザーのマイクロポスト
- 第12章 ユーザーをフォローする
第10章 アカウント有効化とパスワード再設定
第9章では、基本的なUsersリソース (表7.1の標準的なRESTアクションをすべて使用) と、自由度の高い認証 (authentication) および認可 (authorization) システムを作成しました。本章ではこのシステムの仕上げとして、互いに強く関連している2つの機能、すなわちアカウントの有効化 (アクティベーション: 新規ユーザーのメールアドレスが有効であることを確認する機能) と、パスワードの再設定 (パスワードを忘れてしまったユーザー向けの機能) を実装することにします。これらの機能ごとに新しいリソースを作成し、それぞれのコントローラ/ルーティング/データベース移行の例について見ていくことにします。その途中で、Railsの開発環境や本番環境からメールを送信する方法についても学習します。最後に、2つの機能を完全に連携させます。パスワードを再設定すると、パスワード再設定用のリンクがメールで送信され、その宛先メールアドレスが有効であることは最初のアカウント有効化で確認済みである、といった具合です1。
10.1 アカウントの有効化
今の状態では、新しくアカウントを登録したユーザーはアカウントに対するフルアクセス権限を持っています (第7章) が、このままではいかにも大雑把です。そこで、アカウントを有効化する手順を実装して、ユーザーが登録に使用したメールアドレスを本当に所有、管理しているのかどうかを確認するようにしましょう。これを行うおおよその流れは、有効化トークンやダイジェストをユーザーと関連付け、ユーザーにメールを送信し、そのメールにはトークンを含むリンクを記載しておき、ユーザーがそのリンクをクリックすると有効化できるようになる、というものです。
アカウントを有効化する段取りは、ユーザーログイン (8.2)、特にユーザーの記憶 (8.4) と似ています。基本的な手順は次のようになります。
- ユーザーの初期状態は「有効化されていない」(unactivated) にしておく。
- ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する。
- 有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく2。
- ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。
- ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」(activated) に変更する。
都合の良いことに、今回実装するアカウント有効化やパスワード再設定の仕組みと、以前に実装したパスワードや記憶トークンの仕組みにはよく似た点が多いので、多くのアイデアを使い回すことができます (User.digest
メソッド、User.new_token
メソッド、改造版のuser.authenticated?
メソッドなど)。表10.1に両者の似ている点を示します (10.2のパスワード再設定も含む)。10.1.3の表10.1を元に、より一般性の高いauthenticated?
メソッドを定義することにします。
検索キー | 文字列 | ダイジェスト | 認証 |
email |
password |
password_digest |
authenticate(password) |
id |
remember_token |
remember_digest |
authenticated?(:remember, token) |
email |
activation_token |
activation_digest |
authenticated?(:activation, token) |
email |
reset_token |
reset_digest |
authenticated?(:reset, token) |
それではいつものように、Gitで新機能用のトピックブランチを作成しましょう。10.3で説明したとおり、アカウントの有効化とパスワードの再設定では、メールの設定部分に共通するところがありますので、その部分を両機能に適用してからGitのmasterにマージします。こうすることで共通のトピックブランチを使えるようになり、便利です。
$ git checkout master
$ git checkout -b account-activation-password-reset
10.1.1 AccountActivationsリソース
セッション機能 (8.1) を使用して、アカウントの有効化という作業を「リソース」としてモデル化することにします。アカウントの有効化リソースはActive Recordのモデルとはこの際関係ないので、両者を関連付けることはしません。その代わりに、この作業に必要なデータ (有効化トークンや有効化ステータスなど) をUserモデルに追加することにします。ただし、アカウント有効化におけるやりとりでは、標準のREST URLを使用するようにします。有効化用のリンクにアクセスするとユーザーの有効化ステータスが必然的に変わるので、標準的なedit
アクションを使用することにします3。では始めましょう。AccountActivationsコントローラを以下のコマンドで生成します4。
$ rails generate controller AccountActivations --no-test-framework
上のコマンドでは、テストを生成しないというオプションを指定していることにご注目ください。著者はコントローラのテストよりも統合テスト (10.1.4) の方が望ましいと考えているので、コントローラのテストを生成しないようにしているのです。
有効化メールでは以下の形式のURLを使用します。
edit_account_activation_url(activation_token, ...)
これは、edit
アクションへの名前付きルートが必要になるということです。そのためのresources
行を追加します (リスト10.1)。
Rails.application.routes.draw do
root 'static_pages#home'
get 'help' => 'static_pages#help'
get 'about' => 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
end
続いて、一意の有効化トークンがユーザー有効化に必要です。アカウント有効化のセキュリティは、パスワードや記憶トークンやパスワードの再設定 (10.2) に比べると注意点が少なくて済みます。後者への攻撃が成功すれば全権を奪われてしまう可能性がありますが、前者も有効化トークンをハッシュ化しておかなければアカウントに攻撃を受ける可能性があります5。そこで、8.4の記憶トークンの例に従って、一般公開してもよい「仮想トークン」と、データベース内にのみ保存する「ハッシュダイジェスト」を組み合わせるとにします。これにより、以下を使用して有効化トークンにアクセスし、
user.activation_token
以下のようなコードでユーザーを認証できるようになります。
user.authenticated?(:activation, token)
(これを行うにはリスト8.33のauthenticated?
メソッドを改良する必要があります)。 続いて、activated
属性を追加して論理値 (true/false) を取るようにします。これで、9.4.1で説明した自動生成の論理値メソッドと同じような感じで、ユーザーが有効であるかどうかをテストできるようになります。
if user.activated? ...
最後に、本チュートリアルで使うことはありませんが、ユーザーを有効にしたときの日時も念のために記録しておきます。変更後のデータモデルは図10.1のようになります。
以下のマイグレーションをコマンドラインで実行して図10.1のデータモデルを追加すると、3つの属性が新しく追加されます。
$ rails generate migration add_activation_to_users \
> activation_digest:string activated:boolean activated_at:datetime
(上記の2行目にある '>' という文字は、改行を示すためにシェルが自動的に挿入する文字です。手動で入力しないよう、注意してください) admin
属性 (リスト9.50) の時と同様に、activated
属性のデフォルトの論理値をfalse
にします (リスト10.2)。
class AddActivationToUsers < ActiveRecord::Migration
def change
add_column :users, :activation_digest, :string
add_column :users, :activated, :boolean, default: false
add_column :users, :activated_at, :datetime
end
end
いつものようにマイグレーションを実行します。
$ bundle exec rake db:migrate
ユーザーが新しい登録を完了するためには必ずアカウントの有効化が必要になるのですから、有効化トークンや有効化ダイジェストはユーザーオブジェクトが作成される前に作成しておく必要があります。これによく似た状況を6.2.5でも説明しました。メールアドレスをデータベースに保存する前に、メールアドレスを全部小文字に変換する必要があったのでした。あのときは、before_save
コールバックにdowncase
メソッドをバインドしました (リスト6.31)。オブジェクトにbefore_save
コールバックを用意しておくと、オブジェクトが保存される直前、オブジェクトの作成時や更新時にそのコールバックが呼び出されます。しかし今回は、オブジェクトが作成されたときだけコールバックを呼び出したいのです。それ以外のときには呼び出したくないのです。そこでbefore_create
コールバックが必要になります。このコールバックは以下のように定義できます。
before_create :create_activation_digest
上のコードはメソッド参照と呼ばれるもので、こうすることでRailsはcreate_activation_digest
というメソッドを探し、ユーザーを作成する前に実行するようになります (リスト6.31では、before_save
に明示的にブロックを渡していましたが、メソッド参照の方が一般にお勧めできます)。 create_activation_digest
メソッド自体はUserモデル内でしか使用しないので、外部に公開する必要はありません。7.3.2のときと同じようにprivate
キーワードを指定して、このメソッドをRuby流に隠蔽します。
private
def create_activation_digest
# トークンとダイジェストを作成する.
end
クラス内でprivate
キーワードより下に記述したメソッドは自動的に非公開となります。このことはコンソールセッションですぐ確かめられます。
$ rails console
>> User.first.create_activation_digest
NoMethodError: private method `create_activation_digest' called for #<User>
今回before_create
コールバックを使用する目的は、トークンとそれに対応するダイジェストを割り当てるためです。割り当ては以下のように行うことができます。
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
このコードでは、記憶トークンで使用したトークンのメソッドやダイジェストのメソッドをストレートに使いまわしています。リスト8.32のremember
メソッドと比べてみましょう。
# 永続セッションのためにユーザーをデータベースに記憶する
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
主な違いは、後者のupdate_attribute
の使い方にあります。この違いは、記憶トークンやダイジェストは既にデータベースにいるユーザーのために作成される (その分やりやすい) のに対し、before_create
コールバックの方はユーザーが作成される前に呼び出される (その分面倒) ことが原因です。このコールバックがあることで、(リスト7.17でユーザー登録を行ったときに)User.new
で新しいユーザーが定義されると、activation_token
属性やactivation_digest
属性を自動的に得られます。後者のactivation_digest属性は既にデータベースのカラムとの関連付けができあがっている (図10.1) ので、ユーザーが保存されるときに一緒に自動保存されます。
上で説明したことをUserモデルに実装するとリスト10.3のようになります。有効化トークンは本質的に仮のものでなければならないので、このモデルのattr_accessor
にもうひとつ追加しました。メールアドレスを小文字にするときにもメソッド参照が使用される機会があることにご注目ください。
class User < ActiveRecord::Base
attr_accessor :remember_token, :activation_token
before_save :downcase_email
before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }
.
.
.
private
# メールアドレスをすべて小文字にする
def downcase_email
self.email = email.downcase
end
# 有効化トークンとダイジェストを作成および代入する
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
先に進む前に、サンプルデータとフィクスチャも更新し、テスト時のサンプルとユーザーを事前に有効化しておきましょう (リスト10.4とリスト10.5)。なお、Time.zone.now
はRailsの組み込みヘルパーであり、サーバーのタイムゾーンに応じたタイムスタンプを返します。
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true,
activated: true,
activated_at: Time.zone.now)
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password,
activated: true,
activated_at: Time.zone.now)
end
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true
activated: true
activated_at: <%= Time.zone.now %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
activated: true
activated_at: <%= Time.zone.now %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
activated: true
activated_at: <%= Time.zone.now %>
mallory:
name: Mallory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
activated: true
activated_at: <%= Time.zone.now %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
activated: true
activated_at: <%= Time.zone.now %>
<% end %>
いつものようにデータベースを初期化して、サンプルデータを再度生成し直し、リスト10.4の変更を反映します。
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
10.1.2 AccountActivationsメイラーメソッド
データのモデル化が終わったので、今度はアカウント有効化メールの送信に必要なコードを追加しましょう。このメソッドではAction Mailerライブラリを使用してUserのメイラーを追加します。このメイラーはUsersコントローラのcreate
アクションで有効化リンクをメール送信するために使用します。メイラーの構成はコントローラのアクションとよく似ており、メールのテンプレートをビューと同じ要領で定義できます。この節ではメイラーとビューを定義して、有効化トークンとメールアドレス (=有効にするアカウントのアドレス) を含むリンクをその中で使用します。
メイラーは、モデルやコントローラと同様にrails generate
で生成できます。
$ rails generate mailer UserMailer account_activation password_reset
上のコマンドを実行したことで、本節で必要となるaccount_activation
メソッドと、次節 (10.2) で必要となるpassword_reset
メソッドが生成されました。
生成したメイラーごとに、ビューテンプレートが2つずつ生成されます。1つはテキストメール用のテンプレート、1つはHTMLメール用テンプレートです。アカウント有効化メイラーメソッドのテンプレートをリスト10.6とリスト10.7に示します。
UserMailer#account_activation
<%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erb
<h1>UserMailer#account_activation</h1>
<p>
<%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb
</p>
生成されたメイラーの動作を簡単に追ってみましょう (リスト10.8とリスト10.9)。リスト10.8には、デフォルトのfrom
アドレス (アプリケーション全体で共通) があります。リスト10.9の各メソッドには宛先メールアドレスもあります。リスト10.8ではメールのフォーマットに対応するメイラーレイアウトも使用されています。なお本チュートリアルの説明には直接関係ありませんが、生成されるHTMLメイラーのレイアウトやテキストメイラーのレイアウトはapp/views/layouts
で確認できます。生成されたコードにはインスタンス変数@greeting
も含まれています。このインスタンス変数は、ちょうど普通のビューでコントローラのインスタンス変数を利用できるのと同じように、メイラービューで利用できます。
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout 'mailer'
end
class UserMailer < ApplicationMailer
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.user_mailer.account_activation.subject
#
def account_activation
@greeting = "Hi"
mail to: "to@example.org"
end
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.user_mailer.password_reset.subject
#
def password_reset
@greeting = "Hi"
mail to: "to@example.org"
end
end
最初に、生成されたテンプレートをカスタマイズして、実際に有効化メールで使えるようにします (リスト10.10)。次に、ユーザーを含むインスタンス変数を作成してビューで使えるようにし、user.email
にメール送信します (リスト10.11)。リスト10.11では、mail
にsubject
キーも引数として渡しています。この値はメールの件名になります。
from
アドレスを使用するアプリケーションメイラー app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "noreply@example.com"
layout 'mailer'
end
class UserMailer < ApplicationMailer
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
def password_reset
@greeting = "Hi"
mail to: "to@example.org"
end
end
テンプレートビューは、通常のビューと同様ERBで自由にカスタマイズできます。ここでは挨拶文にユーザー名を含め、カスタムの有効化リンクを追加します。この後、Railsサーバーでユーザーをメールアドレスで検索して有効化トークンを認証できるようにしたいので、リンクにはメールアドレスとトークンを両方含めておく必要があります。AccountActivationsリソースで有効化をモデル化したので、トークン自体はリスト10.1で定義した名前付きルートの引数で使用されます。
edit_account_activation_url(@user.activation_token, ...)
ここで思い出してみましょう。
edit_user_url(user)
上のメソッドは、以下の形式のURLを生成します。
http://www.example.com/users/1/edit
これに対応するアカウント有効化リンクのベースURLは以下のようになります。
http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit
上のURLの「q5lt38hQDc_959PVoo6b7A
」という部分はnew_token
メソッドで生成されたものです (リスト8.31)。URLで使用できるようにBase64でエンコードされています。これはちょうど/users/1/editの「1」のようなユーザーIDと同じ役割を果たします。このトークンは、特にActivationsコントローラのedit
アクションではparams
ハッシュでparams[:id]
として参照できます。
クエリパラメータを使用して、このURLにメールアドレスもうまく組み込んでみましょう。クエリパラメータとは、URLの末尾で疑問符「?」に続けてキーと値のペアを記述したものです6。
account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com
このとき、メールアドレスの「@」記号がURLでは「%40
」となっている点に注目してください。これは「エスケープ」と呼ばれる手法で、ここではURLで通常使用できない文字を含めるために使用されます。Railsでクエリパラメータを設定するには、以下のように名前付きルートにハッシュを追加します。
edit_account_activation_url(@user.activation_token, email: @user.email)
このようにして名前付きルートでクエリパラメータを定義すると、Railsが特殊な文字を自動的にエスケープしてくれます。コントローラでparams[:email]
からメールアドレスを取り出すときには、自動的にエスケープを解除してくれます。
ここまでできれば、リスト10.11で定義した@user
インスタンス変数、editへの名前付きルート、ERBを組み合わせて、必要なリンクを作成できます (リスト10.12とリスト10.13)。リスト10.13のHTMLテンプレートでは、正しいリンクを組立てるためにlink_to
メソッドを使用していることにご注目ください。
Hi <%= @user.name %>,
Welcome to the Sample App! Click on the link below to activate your account:
<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
<h1>Sample App</h1>
<p>Hi <%= @user.name %>,</p>
<p>
Welcome to the Sample App!Click on the link below to activate your account:
</p>
<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
email: @user.email) %>
リスト10.12やリスト10.13で定義したテンプレートの実際の表示を簡単に確認するために、メールプレビューという裏技を使ってみましょう。Railsでは、特殊なURLにアクセスするとメールのメッセージをその場でプレビューすることができます。メールを実際に送信しなくてもよいので大変便利です。これを利用するには、アプリケーションのdevelopment環境の設定に手を加える必要があります (リスト10.14)。
Rails.application.configure do
.
.
.
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :test
host = 'example.com'
config.action_mailer.default_url_options = { host: host }
.
.
.
end
リスト10.14のホスト名「’example.com’
」の部分は各自のdevelopment環境に合わせて変更してください。たとえば、著者のシステムでは以下のどちらでも動くようになっています (クラウドIDEとローカルサーバーで使い分けています)。
host = 'rails-tutorial-c9-mhartl.c9.io' # クラウド IDE
または
host = 'localhost:3000' # ローカルサーバー
developmentサーバーを再起動してリスト10.14の設定を読み込んだら、次は10.1.2で自動生成したUserメイラーのプレビューファイルの更新が必要です (リスト10.15)。
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
UserMailer.account_activation
end
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
UserMailer.password_reset
end
end
リスト10.11で定義したaccount_activation
の引数には有効な (=実在する) ユーザーオブジェクトを渡す必要があるため、リスト10.15はこのままでは動きません。これを回避するために、user
変数をdevelopmentデータベースの最初のユーザーになるように定義して、それをUserMailer.account_activation
に引数として渡します (リスト10.16)。リスト10.16ではuser.activation_token
にも値を代入している点にご注目ください。リスト10.12やリスト10.13のアカウント有効化テンプレートではアカウント有効化トークンが必要なので、代入は省略できません。なお、activation_token
は仮の属性でしかないので (10.1.1)、データベースのユーザーはこの値を実際には持っていません。
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
user = User.first
user.activation_token = User.new_token
UserMailer.account_activation(user)
end
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
UserMailer.password_reset
end
end
リスト10.16のプレビューコードを実装すると、指定のURLでアカウント有効化メールをプレビューできるようになります (クラウドIDEをご利用の場合は、localhost:3000
の部分を対応するベースURLに置き換えてください)。HTMLメールとテキストメールのプレビューを図10.2と図10.3に示します。
最後に、このメールプレビューのテストも作成して、プレビューをダブルチェックできるようにします。便利なテスト例がRailsによって自動生成されているので (リスト10.17)、これを利用すればテストの作成は割と簡単です。
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
test "account_activation" do
mail = UserMailer.account_activation
assert_equal "Account activation", mail.subject
assert_equal ["to@example.org"], mail.to
assert_equal ["from@example.com"], mail.from
assert_match "Hi", mail.body.encoded
end
test "password_reset" do
mail = UserMailer.password_reset
assert_equal "Password reset", mail.subject
assert_equal ["to@example.org"], mail.to
assert_equal ["from@example.com"], mail.from
assert_match "Hi", mail.body.encoded
end
end
リスト10.17のテストでは、assert_match
という非常に強力なメソッドが使用されています。これを使えば、正規表現で文字列をテストできます。
assert_match 'foo', 'foobar' # true
assert_match 'baz', 'foobar' # false
assert_match /\w+/, 'foobar' # true
assert_match /\w+/, '$#!*+@' # false
リスト10.18のテストでは、assert_match
メソッドを使用して名前、有効化トークン、エスケープ済みメールアドレスがメール本文に含まれているかどうかをテストします。最後にもうひとつ小技をお教えします。
CGI::escape(user.email)
このメソッドを使うと、ユーザーのメールのテストをエスケープできます7。
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
test "account_activation" do
user = users(:michael)
user.activation_token = User.new_token
mail = UserMailer.account_activation(user)
assert_equal "Account activation", mail.subject
assert_equal [user.email], mail.to
assert_equal ["noreply@example.com"], mail.from
assert_match user.name, mail.body.encoded
assert_match user.activation_token, mail.body.encoded
assert_match CGI::escape(user.email), mail.body.encoded
end
end
リスト10.18のテストコードでは、フィクスチャユーザーに有効化トークンを追加している点にご注目ください。追加しない場合は空白になります。
このテストがパスするには、テストファイル内のドメイン名を正しく設定する必要があります (リスト10.19)。
Rails.application.configure do
.
.
.
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: 'example.com' }
.
.
.
end
上のコードを使用すると、テストはGREENになるはずです。
$ bundle exec rake test:mailers
あとはユーザー登録を行うcreate
アクションに数行追加するだけで、メイラーをアプリケーションで実際に使うことができます (リスト10.21)。リスト10.21では、登録時のリダイレクトの挙動が変更されている点にご注意ください。変更前は、ユーザーのプロファイルページ (7.4) にリダイレクトしていましたが、アカウント有効化を実装するうえでは無意味な動作なので、リダイレクト先をルートURLに変更してあります。
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
UserMailer.account_activation(@user).deliver_now
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end
.
.
.
end
リスト10.21ではリダイレクト先をプロファイルページからルートURLに変更し、かつユーザーは以前のようにログインしないようになっています。したがって、アプリケーションの動作が仮に正しくても、現在のテストスイートはREDになります。そこで、失敗が発生するテストの行をひとまずコメントアウトしておきます (リスト10.22)。コメントアウトした部分は、10.1.4でアカウント有効化のテストをパスさせるときに元に戻します。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" }
end
assert_template 'users/new'
assert_select 'div#error_explanation'
assert_select 'div.field_with_errors'
end
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post_via_redirect users_path, user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" }
end
# assert_template 'users/show'
# assert is_logged_in?
end
end
この状態で実際に新規ユーザーとして登録してみると、リダイレクトされて図10.4のようになり、リスト10.23のようなメールが生成されます。ただし、実際にメールが生成されるわけではないのでご注意ください。ここに引用したのはサーバーログに出力されたメールです (メールが見えるまで多少スクロールが必要でしょう)。production環境で実際にメール送信する方法については10.3で説明します。
Sent mail to michael@michaelhartl.com (931.6ms)
Date: Wed, 03 Sep 2014 19:47:18 +0000
From: noreply@example.com
To: michael@michaelhartl.com
Message-ID: <540770474e16_61d3fd1914f4cd0300a0@mhartl-rails-tutorial-953753.mail>
Subject: Account activation
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_5407704656b50_61d3fd1914f4cd02996a";
charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_5407704656b50_61d3fd1914f4cd02996a
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
Hi Michael Hartl,
Welcome to the Sample App! Click on the link below to activate your account:
http://rails-tutorial-c9-mhartl.c9.io/account_activations/
fFb_F94mgQtmlSvRFGsITw/edit?email=michael%40michaelhartl.com
----==_mimepart_5407704656b50_61d3fd1914f4cd02996a
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<h1>Sample App</h1>
<p>Hi Michael Hartl,</p>
<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>
<a href="http://rails-tutorial-c9-mhartl.c9.io/account_activations/
fFb_F94mgQtmlSvRFGsITw/edit?email=michael%40michaelhartl.com">Activate</a>
----==_mimepart_5407704656b50_61d3fd1914f4cd02996a--
10.1.3 アカウントを有効化する
リスト10.23のとおりにメールが生成できたら、今度はAccountActivationsコントローラのedit
アクションを書いて、実際にユーザーを有効化できるようにする必要があります。ここで、有効化トークンとメールをそれぞれparams[:id]
とparams[:email]
で参照できる (10.1.2) ことを思い出してみましょう。パスワードのモデル (リスト8.5) と記憶トークン (リスト8.36) で学んだことを元に、次のようなコードでユーザーを検索して認証することにします。
user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])
(この後、上の式にひとつ論理値を追加します。何が追加されるか考えてみましょう)。
上のコードで使用しているauthenticated?
メソッドは、アカウント有効化のダイジェストと、渡されたトークンが一致するかどうかをチェックします。ただし、このメソッドは記憶トークン (リスト8.33) 用なので今は正常に動作しません。
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
remember_digest
はUserモデルの属性であり、モデル内では以下のように書き換えることができます。
self.remember_digest
上をどうにかして変数として扱いたいのです。そこで以下を呼び出すことにします。
self.activation_token
authenticated?
に該当のパラメータを渡す代わりに、上のようにします。
この一見不思議な手法は「メタプログラミング」の最初の例になります。メタプログラミングを一言で言うと「プログラムでプログラムを作成する」ことです。メタプログラミングはRubyが有するきわめて強力な機能であり、Railsの一見魔法のような機能 (訳注: 「黒魔術」と呼ばれることもあります) の多くは、Rubyのメタプログラミングによって実現されています。ここで重要なのは、send
メソッドの強力きわまる機能です。このメソッドは、与えられたオブジェクトに「メッセージを送る」ことによって、メソッド呼び出しに自由に名前を付けることができます。たとえば、このコンソールセッションでネイティブのRubyオブジェクトにsend
メソッドを実行して、配列の長さを得るとします。
$ rails console
>> a = [1, 2, 3]
>> a.length
=> 3
>> a.send(:length)
=> 3
>> a.send('length')
=> 3
上のコードで、シンボル:length
や文字列’length’
をsend
メソッドに渡していますが、これは与えられたオブジェクト (ここではa) のlength
メソッドを呼び出すことと完全に同等です。もうひとつ例をお見せします。データベースの最初のユーザーが持つactivation_digest
属性にアクセスしてみます。
>> user = User.first
>> user.activation_digest
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> user.send(:activation_digest)
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> user.send('activation_digest')
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> attribute = :activation
>> user.send("#{attribute}_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
最後の例では、シンボル:activation
に等しいattribute
変数を定義し、文字列の式展開 (interpolation) を使用して引数を正しく組み立ててから、send
に渡していることです。文字列’activation’
でも同じことができますが、Rubyではシンボルを使う方が普通です。
"#{attribute}_digest"
シンボルと文字列どちらを使用した場合にも、上のコードは以下のようになります。
"activation_digest"
文字列は式展開されます (7.4.2でシンボルが式展開されて文字列になったことを思い出しましょう)。
send
メソッドの動作原理がわかったので、それに基いてauthenticated?
メソッドを書き換えます。
def authenticated?(remember_token)
digest = self.send('remember_digest')
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(remember_token)
end
この配置されたテンプレートで、関数の引数にダイジェスト名を追加してこのメソッドを一般化し、続いて上のように文字列の式展開を使用します。
def authenticated?(attribute, token)
digest = self.send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
(上では2番目の引数token
の名前を変更して、メソッドが一般化されたことをあえて強調しています)。このコードはモデル内にあるのでself
は省略できます。最終的にRubyらしく書かれたコードは次のようになります。
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
ここまでできれば、以下のように呼び出すことでauthenticated?
の従来の振舞いを再現できます。
user.authenticated?(:remember, remember_token)
以上の説明を実際のUserモデルに適用してできた、一般化されたauthenticated?
メソッドをリスト10.24に示します。
authenticated?
メソッド RED app/models/user.rb
class User < ActiveRecord::Base
.
.
.
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
.
.
.
end
リスト10.24のキャプションに示されているとおり、テストスイートはREDになります。
$ bundle exec rake test
テストが失敗する理由は、current_user
メソッド (リスト8.36) とnil
ダイジェストのテスト (リスト8.43) の両方で、authenticated?
が古いままになっており、引数も2つではなくまだ1つのままです。これを解消するために両者を更新して、新しい一般的なメソッドを使用するようにします (リスト10.26とリスト10.27)。
current_user
内の一般化したauthenticated?
メソッド app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
# 現在ログイン中のユーザーを返す (いる場合)
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
@current_user = user
end
end
end
.
.
.
end
authenticated?
メソッド GREEN test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "authenticated? should return false for a user with nil digest" do
assert_not @user.authenticated?(:remember, '')
end
end
今度はテストがGREENになるはずです。
$ bundle exec rake test
コードにこのようなリファクタリングを施すと非常にエラーが発生しやすくなるので (訳注: 黒魔術と呼ばれる理由でもあります)、しっかりしたテストスイートが不可欠です。8.4.2や8.4.6でよいテストを書くためにあえてトラブルを発生させてみたのはそうした理由からです。
authenticated?
がリスト10.24のようになったことで、やっとedit
アクションを書く準備ができました。このアクションは、params
ハッシュで与えられたメールアドレスに対応するユーザーを認証します。ユーザーが有効であることを確認する中核部分は以下のようになります。
if user && !user.activated? && user.authenticated?(:activation, params[:id])
!user.activated?
という記述にご注目ください。先ほど「ひとつ論理値を追加します」と言っていたのはこのことです。このコードは、既に有効になっているユーザーを誤って再度有効化しないために必要です。正当であろうとなかろうと、有効化が行われるとユーザーはログイン状態になります。もしこのコードがなければ、攻撃者がユーザーの有効化リンクを後から盗みだしてクリックするだけで、本当のユーザーとしてログインできてしまいます。そうした攻撃を防ぐためにこのコードは非常に重要です。
上の論理値に基いてユーザーを認証するには、ユーザーを認証してからactivated_at
タイムスタンプを更新する必要があります。(update_attributesではなくupdate_attributeを実行していることに注目してください。update_attributesだとバリデーションが実行されてしまうため、今回のようにパスワードを入力していない状態で更新すると、バリデーションで失敗してしまいます。)
user.update_attribute(:activated, true)
user.update_attribute(:activated_at, Time.zone.now)
上のコードをedit
アクション (リスト10.29) で使用します。リスト10.29では有効化トークンが無効だった場合の処理も行われている点にご注目ください。トークンが無効になるようなことは実際にはめったにありませんが、もしそうなった場合はルートURLにリダイレクトされる仕組みです。
edit
アクション app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.update_attribute(:activated, true)
user.update_attribute(:activated_at, Time.zone.now)
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end
end
リスト10.29のコードを使用すると、リスト10.23にあるURLを貼り付けてユーザーを有効化できます。著者のシステム上では、以下のURLをブラウザで開くと、
http://rails-tutorial-c9-mhartl.c9.io/account_activations/
fFb_F94mgQtmlSvRFGsITw/edit?email=michael%40michaelhartl.com
図10.5のようになりました。
もちろん、この時点ではユーザーのログイン方法を変更していないので、ユーザーの有効化にはまだ何の意味もありません。ユーザーの有効化が役に立つためには、ユーザーが有効である場合にのみログインできるようにログイン方法を変更する必要があります。リスト10.30に示したように、これを行うにはuser.activated?
がtrueの場合にのみログインを許可し、そうでない場合はルートURLにリダイレクトしてwarning
で警告を表示します (図10.6)。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
if user.activated?
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "Account not activated. "
message += "Check your email for the activation link."
flash[:warning] = message
redirect_to root_url
end
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end
これで、ユーザー有効化機能のおおまかな部分については実装できました (改良すべき点として、有効化されていないユーザーが表示されないようにする必要もあるのですが、これは10.5の課題に回すことにします)。次の10.1.4でテストをもう少し追加し、リファクタリングを少々施せば完了です。
10.1.4 有効化のテストとリファクタリング
この節では、アカウント有効化の統合テストを追加します。正しい情報でユーザー登録を行った場合のテスト (リスト7.26) は既にあるので、7.4.4で開発したテストに若干手を加えることにします。追加される行数はそこそこ多いのですが、基本的に素直なので心配はありません。リスト10.31をご覧ください。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
end
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" }
end
assert_template 'users/new'
assert_select 'div#error_explanation'
assert_select 'div.field_with_errors'
end
test "valid signup information with account activation" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" }
end
assert_equal 1, ActionMailer::Base.deliveries.size
user = assigns(:user)
assert_not user.activated?
# 有効化していない状態でログインしてみる
log_in_as(user)
assert_not is_logged_in?
# 有効化トークンが不正な場合
get edit_account_activation_path("invalid token")
assert_not is_logged_in?
# トークンは正しいがメールアドレスが無効な場合
get edit_account_activation_path(user.activation_token, email: 'wrong')
assert_not is_logged_in?
# 有効化トークンが正しい場合
get edit_account_activation_path(user.activation_token, email: user.email)
assert user.reload.activated?
follow_redirect!
assert_template 'users/show'
assert is_logged_in?
end
end
リスト10.31のコードは分量が多いように見えますが、本当に重要な部分は以下の1行です。
assert_equal 1, ActionMailer::Base.deliveries.size
上のコードは、配信されたメッセージがきっかり1つであるかどうかを確認します。配列deliveries
はグローバルなので、setup
メソッドでこれを初期化しておかないと、並行して行われる他のテストでメールが配信されたときにエラーで中断してしまいます (10.2.5でも似た事例をご紹介します)。リスト10.31のassigns
メソッドは本チュートリアル初登場です。第8章の演習 (8.6) で説明したように、assigns
メソッドを使用すると、対応するアクション内にあるインスタンス変数にアクセスできるようになります。たとえば、Usersコントローラのcreate
アクションでは@user
というインスタンス変数が定義されています (リスト10.21) ので、テストでassigns(:user)
とするとこのインスタンス変数にアクセスできるようになります。最後に、リスト10.22でコメントアウトしておいた行をリスト10.31で元に戻していることにご注意ください。
これでテストスイートはGREENになるはずです。
$ bundle exec rake test
リスト10.31のテストができたので、ユーザー操作の一部をコントローラからモデルに移動するというささやかなリファクタリングを行う準備ができました。ここでは特に、activate
メソッドを作成してユーザーの有効化属性を更新し、send_activation_email
メソッドを作成して有効化メールを送信します。この新しいメソッドをリスト10.33に示します。また、リファクタリングされたアプリケーションコードをリスト10.34とリスト10.35に示します。
class User < ActiveRecord::Base
.
.
.
# アカウントを有効にする
def activate
update_attribute(:activated, true)
update_attribute(:activated_at, Time.zone.now)
end
# 有効化用のメールを送信する
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
private
.
.
.
end
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
@user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end
.
.
.
end
class AccountActivationsController < ApplicationController
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.activate
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end
end
リスト10.33ではuser.
という記法を使用していないことにご注目ください。Userモデルにはそのような変数はないので、これがあるとエラーになります。
-user.update_attribute(:activated, true)
-user.update_attribute(:activated_at, Time.zone.now)
+update_attribute(:activated, true)
+update_attribute(:activated_at, Time.zone.now)
(user
をself
に切り替えるという手もあるのですが、self
はモデル内では必須ではないと6.2.5で解説したことを思い出しましょう)。Userメイラー内の呼び出しでは、@user
がself
に変更されている点にもご注目ください。
-UserMailer.account_activation(@user).deliver_now
+UserMailer.account_activation(self).deliver_now
どんなに簡単なリファクタリングであっても、この手の変更はつい忘れてしまうものです。テストをきちんと書いておけば、この種の見落としを検出できます。以上でテストスイートはGREENになるはずです。
$ bundle exec rake test
ついにアカウントの有効化を実装できました。きりのよい所でGitにコミットしておきましょう。
$ git add -A
$ git commit -m "Add account activations"
10.2 パスワードの再設定
アカウント有効化の実装が完了し、ユーザーのメールアドレスが正しいことを確認できるようになったので、今度はユーザーがパスワードを忘れてしまった場合に対応できるようにしましょう。実際にやってみるとわかると思いますが、パスワード再設定の仕組みは、アカウント有効化と似ている部分が多く、10.1で学んだ手法の多くをここでも適用できます。とは言うものの、最初の部分はそれなりに異なります。アカウントの有効化と異なり、パスワードを再設定する場合はビューを1つ変更する必要があり、新しいフォームも2つ (メールレイアウト用と新しいパスワードの送信用) 必要です。
コードを実際に書く前に、パスワード再設定の想定手順をモックアップ (=スクリーンショット画像を改変して作った模型) で確かめましょう。まず、サンプルアプリケーションのログインフォームに「forgot password」リンクを追加します (図10.7)。この「forgot password」リンクをクリックするとフォームが表示され、そこにメールアドレスを入力してメールを送信すると、そのメールにパスワード再設定用のリンクが記載されています (図10.8)。この再設定用のリンクをクリックすると、ユーザーのパスワードを再設定してよいかどうかの確認を求めるフォームが表示されます (図10.9)。
アカウント有効化の際と似ていて、PasswordResetsリソースを作成して、再設定用のトークンとそれに対応するダイジェストを保存するのが今回の目的となります。全体の流れは以下のとおりです。
- ユーザーがパスワードの再設定をリクエストすると、ユーザーが送信したメールアドレスをキーにしてデータベースからユーザーを見つける。
- 該当のメールアドレスがデータベースにある場合は、再設定用トークンとそれに対応する再設定用ダイジェストを生成する。
- 再設定用ダイジェストはデータベースに保存しておき、再設定用トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく。
- ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、データベース内に保存しておいた再設定用ダイジェストと比較することでトークンを認証する。
- 認証に成功したら、パスワード変更用のフォームをユーザーに表示する。
10.2.1 PasswordResetsリソース
アカウント有効化 (10.1.1) の場合と同様、最初に新しいリソースで使用するコントローラを生成します。
$ rails generate controller PasswordResets new edit --no-test-framework
10.1.1のときと同様にテストを生成しないようにするフラグを追加し、代わりに10.1.4の統合テストを生成するようにします。
新しいパスワードを再設定するためのフォーム (図10.8) と、Userモデル内のパスワードを変更するためのフォーム (図10.9) が両方必要になるので、今回はnew
、create
、edit
、update
のルーティングも必要になります。この変更は、ルーティングファイルのresources
行で行います (リスト10.37)。
Rails.application.routes.draw do
root 'static_pages#home'
get 'help' => 'static_pages#help'
get 'about' => 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]
end
リスト10.37のコードはRESTfulのルーティング (表10.2) に従っています。特に、表10.2の最初のルーティングでは「forgot password」フォームへのリンク作成に以下を使用しています。
new_password_reset_path
HTTPリクエスト | URL | アクション | 名前付きルート |
GET | /password_resets/new | new |
new_password_reset_path |
POST | /password_resets | create |
password_resets_path |
GET | /password_resets/<トークン>/edit | edit |
edit_password_reset_path(トークン) |
PATCH | /password_resets/<トークン> | update |
password_reset_path(トークン) |
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= link_to "(forgot password)", new_password_reset_path %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
パスワード再設定のデータモデルも、アカウント有効化の場合と似ています (図10.1)。記憶トークン (8.4) とアカウント有効化トークン (10.1) で設定したパターンに従い、パスワードの再設定でも、メールに記載する仮想の再設定用トークンと、ユーザーの取得で使う再設定用ダイジェストをペアにすることにします。トークンをハッシュ化せずにデータベースに保存すると、トークンが攻撃者によってデータベースから読み出されたときにセキュリティ上の問題が生じます: 攻撃者がユーザーのメールアドレスにパスワード再設定のリクエストを送信し、このメールと盗んだトークンを使用して攻撃者がパスワード再設定リンクを開けば、アカウントを奪い取ることができてしまいます。従って、パスワードの再設定では必ずダイジェストを使用してください。セキュリティ上の注意点をもうひとつ。再設定用のリンクはなるべく短時間 (数時間以内) で期限切れになるようにしなければなりません。そのために、再設定メールの送信時刻も記録する必要があります。以上に基いてreset_digest
属性とreset_sent_at
属性を追加したユーザーデータベースは図10.11のようになります。
以下を実行して、マイグレーションに図10.11の属性を追加します。
$ rails generate migration add_reset_to_users reset_digest:string \
> reset_sent_at:datetime
(上記の2行目にある '>' という文字は、改行を示すためにシェルが自動的に挿入する文字です。手動で入力しないよう、注意してください) 後はいつものようにマイグレーションを実行します。
$ bundle exec rake db:migrate
10.2.2 PasswordResetsコントローラとフォーム
新しいパスワード再設定の画面を作成するために、前節でActive Recordを使用しないリソースを新規作成したときの手法、つまり、新しいセッションを作成するためのログインフォーム (リスト8.2) をここでも使用することにします。参考までにリスト10.39を再掲したのでご覧ください。
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
新しいパスワード再設定フォームはリスト10.39と多くの共通点がありますが、重要な違いとして、form_for
の呼び出しで使用するリソースとURLが異なっていることと、パスワード属性が省略されていることが挙げられます。変更を反映した結果をリスト10.40と図10.12に示します。
<% 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>
図10.12のフォームから送信を行なった後、メールアドレスをキーとしてユーザーをデータベースから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでデータベースの属性を更新する必要があります。それに続いてルートURLにリダイレクトし、フラッシュメッセージをユーザーに表示します。送信が無効の場合は、ログイン (リスト8.9) と同様にnew
ページを出力してflash.now
メッセージを表示します。変更の結果をリスト10.41に示します。
create
アクション app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
def new
end
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
def edit
end
end
Userモデル内のコードは、before_create
コールバック (リスト10.3) 内で使用されるcreate_activation_digest
メソッドと似ています(リスト10.42)。
class User < ActiveRecord::Base
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
.
.
.
# アカウントを有効にする
def activate
update_attribute(:activated, true)
update_attribute(:activated_at, Time.zone.now)
end
# 有効化用のメールを送信する
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
# パスワード再設定の属性を設定する
def create_reset_digest
self.reset_token = User.new_token
update_attribute(:reset_digest, User.digest(reset_token))
update_attribute(:reset_sent_at, Time.zone.now)
end
# パスワード再設定のメールを送信する
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
private
# メールアドレスをすべて小文字にする
def downcase_email
self.email = email.downcase
end
# 有効化トークンとダイジェストを作成および代入する
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
図10.13に示すように、この時点でのアプリケーションは、無効なメールアドレスを入力した場合に正常に動作します。正しいメールアドレスを送信した場合にもアプリケーションが正常に動作するためには、パスワード再設定のメイラーメソッドを定義する必要があります。
10.2.3 PasswordResetsメイラーメソッド
リスト10.42でパスワード再設定のメールを送信するコードは、以下の部分です。
UserMailer.password_reset(self).deliver_now
上のコードが動作するために必要なパスワード再設定用メイラーメソッドは、10.1.2で開発したアカウント有効化用メイラーメソッドとほぼ同じです。最初にユーザーメイラーにpassword_reset
メソッドを作成し (リスト10.43)、続いてテキストメールのビューテンプレート (リスト10.44) と HTMLメールのビューテンプレート (リスト10.45) をそれぞれ定義します。
class UserMailer < ApplicationMailer
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
def password_reset(user)
@user = user
mail to: user.email, subject: "Password reset"
end
end
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.
<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>
アカウント有効化メールの場合 (10.1.2) と同様、Railsのメールプレビュー機能でパスワード再設定のメールをプレビューしましょう。そのためのコードはリスト10.16と基本的にまったく同じです (リスト10.46)。
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
user = User.first
user.activation_token = User.new_token
UserMailer.account_activation(user)
end
# 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
end
リスト10.46のコードで、HTMLメールとテキストメールをそれぞれプレビューできるようになります (図10.14と図10.15)。
アカウント有効化メイラーメソッドのテスト (リスト10.18) の場合と同様、パスワード再設定用メイラーメソッドのテストを書くことにします (リスト10.47)。ただし、有効化トークンの場合と異なり、パスワード再設定用トークンはビューの中で使用される点にご注意ください。有効化トークンはbefore_create
コールバックでユーザーひとりひとりに対して作成されます (リスト10.3) が、パスワード再設定用トークンの方はユーザーが「forgot password」フォームを送信できた場合にだけ作成されます。この動作は統合テストで自然に行われます (リスト10.54) が、このテストではパスワード再設定用トークンを手動で作成する必要があります。
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
test "account_activation" do
user = users(:michael)
user.activation_token = User.new_token
mail = UserMailer.account_activation(user)
assert_equal "Account activation", mail.subject
assert_equal [user.email], mail.to
assert_equal ["noreply@example.com"], mail.from
assert_match user.name, mail.body.encoded
assert_match user.activation_token, mail.body.encoded
assert_match CGI::escape(user.email), mail.body.encoded
end
test "password_reset" do
user = users(:michael)
user.reset_token = User.new_token
mail = UserMailer.password_reset(user)
assert_equal "Password reset", mail.subject
assert_equal [user.email], mail.to
assert_equal ["noreply@example.com"], mail.from
assert_match user.reset_token, mail.body.encoded
assert_match CGI::escape(user.email), mail.body.encoded
end
end
これでテストスイートはGREENになるはずです。
$ bundle exec rake test
リスト10.43、リスト10.44、リスト10.45のコードを使用すると、正しいメールアドレスを送信したときの画面は図10.16のようになります。このメールはサーバーログではリスト10.49のように表示されます。
Sent mail to michael@michaelhartl.com (66.8ms)
Date: Thu, 04 Sep 2014 01:04:59 +0000
From: noreply@example.com
To: michael@michaelhartl.com
Message-ID: <5407babbee139_8722b257d04576a@mhartl-rails-tutorial-953753.mail>
Subject: Password reset
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_5407babbe3505_8722b257d045617";
charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_5407babbe3505_8722b257d045617
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
To reset your password click the link below:
http://rails-tutorial-c9-mhartl.c9.io/password_resets/3BdBrXeQZSWqFIDRN8cxHA/
edit?email=michael%40michaelhartl.com
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.
----==_mimepart_5407babbe3505_8722b257d045617
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<a href="http://rails-tutorial-c9-mhartl.c9.io/
password_resets/3BdBrXeQZSWqFIDRN8cxHA/
edit?email=michael%40michaelhartl.com">Reset password</a>
<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>
----==_mimepart_5407babbe3505_8722b257d045617--
10.2.4 パスワードを再設定する
以下のようなフォームリンクが動作するためには、
http://example.com/password_resets/3BdBrXeQZSWqFIDRN8cxHA/edit?email=foo%40bar.com
パスワード再設定のフォームが必要です。この作業はユーザーのeditビューでユーザーを更新する (リスト9.2) のと似ていますが、今回はパスワード入力フィールドと確認用フィールドだけを使います。今回は少しだけ面倒な点があります。メールアドレスをキーとしてユーザーを検索するということは、edit
アクションとupdate
アクションの両方でメールアドレスが必要になるということです。例のメールアドレス入りリンクのおかげで、edit
アクションでメールアドレスを取り出すのは問題ありません。しかしフォームを送信するとこの値は消えてしまいます。この値はどこに保持しておくのがよいのでしょうか。このメールアドレスの最適な保存方法は、隠しフィールドとしてページ内に保存することです。これにより、フォームを送信すると他の情報と一緒にメールアドレスが送信されます。これらを反映したものをリスト10.50に示します。
<% 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>
リスト10.50では以下のフォームタグヘルパーを使用している点にご注意ください。
hidden_field_tag :email, @user.email
以下のようないつものヘルパーではありません。
f.hidden_field :email, @user.email
これは、再設定用のリンクをクリックすると、前者ではメールアドレスがparams[:email]
に保存されますが、後者を使用するとparams[:user][:email]
に保存されてしまうからです。
今度は、このフォームを出力 (レンダリング) するためにPasswordResetsコントローラのedit
アクション内で@user
インスタンス変数を定義する必要があります。アカウント有効化 (リスト10.29) の場合と同様、params[:email]
にあるメールアドレスに対応するユーザーをこの変数に保存します。続いて、params[:id]
(リスト10.24で定義した、一般化されたauthenticated?
メソッド) の再設定用トークンを使用して、このユーザーが正当なものである (ユーザーが存在する、有効化されている、認証済みである) ことを確認します。edit
アクションとupdate
アクションのどちらの場合も正当な@user
が存在する必要があるので、いくつかのbeforeフィルタを使用して@userの検索とバリデーションを行います (リスト10.51)。
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 get_user
@user = User.find_by(email: params[:email])
end
# 正しいユーザーを確認する
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id]))
redirect_to root_url
end
end
end
リスト10.51では以下のコードを使用しています。
authenticated?(:reset, params[:id])
上を以下のコードと比べてみましょう。
authenticated?(:remember, cookies[:remember_token])
このコードはリスト10.26で使用されていました。もうひとつ、
authenticated?(:activation, params[:id])
これはリスト10.29で使用されていました。いずれの場合も、表10.1の認証メソッドを完了します。
上のコードを使用することで、リスト10.49のログにあるリンクを開いたときにパスワード再設定のフォームが出力されるようになります。実行結果を図10.17に示します。
リスト10.51のedit
アクションに対応するupdate
アクションを定義するには、4通りの場合分けに対応する必要があります: パスワード再設定の期限が切れている場合、更新に成功した場合、更新が失敗した場合 (パスワードが正しくないなど)、更新が失敗した場合 (一見更新が成功したように見えるがパスワードが2つとも空欄) です。1番目はedit
アクションと update
アクションの両方で対応する必要があるため、論理的にはbeforeフィルタで行うべきです (リスト10.52)。2番目と3番目はメインのif
文の2つの分岐に対応します (リスト10.52)。editフォームはActive Recordモデルオブジェクト (ユーザーなど) を変更するので、エラーメッセージの出力にリスト10.50の一部を共有できます。唯一の例外は、4番目のパスワードが空欄の場合です。現在のUserモデルでは空パスワードが許されている (リスト9.10) ので、空パスワードの場合については明示的に検出および対応する必要があります8。今回は、以下のように@user
オブジェクトのエラーメッセージにエラーを直接追加して対応することにします。
@user.errors.add(:password, "can't be empty")
update
アクション app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
before_action :get_user, only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
before_action :check_expiration, only: [:edit, :update]
def new
end
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
def edit
end
def update
if params[:user][:password].empty?
@user.errors.add(:password, "can't be empty")
render 'edit'
elsif @user.update_attributes(user_params)
log_in @user
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit'
end
end
private
def user_params
params.require(:user).permit(:password, :password_confirmation)
end
# Beforeフィルタ
def get_user
@user = User.find_by(email: params[:email])
end
# 正しいユーザーを確認する
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
end
リスト10.52の実装では以下のコードを使用して、パスワード再設定の期限切れの論理値テストをUserモデルに委譲 (delegate) しています。
@user.password_reset_expired?
上のコードが動作するには、このpassword_reset_expired?
メソッドを定義する必要があります。10.2.3のメールテンプレートのところで説明したように、パスワード再設定の期限を設定し、2時間以上パスワードが再設定されなかった場合は期限切れにする必要があります。これをRubyで表現すると以下のようになります。
reset_sent_at < 2.hours.ago
この「<
」記号を「〜より少ない」と読んでしまうと、「パスワード再設定メール送信時から経過した時間が、2時間より少ない場合」となってしまい、ここで行おうとしていることと反対の意味になってしまいます。「<
」はここでは「〜より早い時刻」と読んでください。これなら「パスワード再設定メールの送信時刻が、現在時刻より2時間以上前の場合」となり、期待どおりの条件となります。そして条件が満たされるとリスト10.53のpassword_reset_expired?
メソッドが実行されます (この比較の公式な証明を10.6に付録として追加しました)。
class User < ActiveRecord::Base
.
.
.
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
private
.
.
.
end
リスト10.53のコードを使用すると、リスト10.52のupdate
アクションが動作するようになります。送信が無効だった場合と有効だった場合の画面をそれぞれ図10.18と図10.19に示します (確認のために2時間も待っていられないので、テストにはもうひとつ分岐を追加しますが、これは10.5の演習に回すことにします)。
10.2.5 パスワードの再設定をテストする
この節では、リスト10.52の2つ (または3つ) の分岐、つまり送信に成功した場合と失敗した場合の統合テストを作成します (前述のとおり、3番目の場合については演習に回します)。まずはパスワード再設定のテストファイルを生成しましょう。
$ rails generate integration_test password_resets
invoke test_unit
create test/integration/password_resets_test.rb
パスワード再設定をテストする手順は、アカウント有効化のテスト (リスト10.31) と多くの共通点がありますが、テストの冒頭部分には次のような違いがあります: 最初に「forgot password」フォームを表示して無効なメールアドレスを送信し、次はそのフォームで有効なメールアドレスを送信します。後者ではパスワード再設定用トークンが作成され、再設定用メールが送信されます。続いて、メールのリンクを開いて無効な情報を送信し、次にそのリンクから有効な情報を送信して、それぞれが期待どおりに動作することを確認します。作成したテストをリスト10.54に示します。このテストはコードリーディングのよい練習台になりますので、みっちりお読みください。
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:michael)
end
test "password resets" do
get new_password_reset_path
assert_template 'password_resets/new'
# メールアドレスが無効
post password_resets_path, password_reset: { email: "" }
assert_not flash.empty?
assert_template 'password_resets/new'
# メールアドレスが有効
post password_resets_path, password_reset: { email: @user.email }
assert_not_equal @user.reset_digest, @user.reload.reset_digest
assert_equal 1, ActionMailer::Base.deliveries.size
assert_not flash.empty?
assert_redirected_to root_url
# パスワード再設定用フォーム
user = assigns(:user)
# メールアドレスが無効
get edit_password_reset_path(user.reset_token, email: "")
assert_redirected_to root_url
# 無効なユーザー
user.toggle!(:activated)
get edit_password_reset_path(user.reset_token, email: user.email)
assert_redirected_to root_url
user.toggle!(:activated)
# メールアドレスが正しく、トークンが無効
get edit_password_reset_path('wrong token', email: user.email)
assert_redirected_to root_url
# メールアドレスもトークンも有効
get edit_password_reset_path(user.reset_token, email: user.email)
assert_template 'password_resets/edit'
assert_select "input[name=email][type=hidden][value=?]", user.email
# 無効なパスワードと確認
patch password_reset_path(user.reset_token),
email: user.email,
user: { password: "foobaz",
password_confirmation: "barquux" }
assert_select 'div#error_explanation'
# パスワードが空
patch password_reset_path(user.reset_token),
email: user.email,
user: { password: "",
password_confirmation: "" }
assert_select 'div#error_explanation'
# 有効なパスワードと確認
patch password_reset_path(user.reset_token),
email: user.email,
user: { password: "foobaz",
password_confirmation: "foobaz" }
assert is_logged_in?
assert_not flash.empty?
assert_redirected_to user
end
end
リスト10.54で使用されているアイデアの大半は、本チュートリアルで既出です。今回の新しい要素はinput
タグぐらいでしょう。
assert_select "input[name=email][type=hidden][value=?]", user.email
上のコードは、input
タグに正しい名前、type="hidden"、メールアドレスがあるかどうかを確認します。
<input id="email" name="email" type="hidden" value="michael@example.com" />
リスト10.54のコードを使用すると、テストコードはGREENになるはずです。
$ bundle exec rake test
10.3 本番環境でのメール
アカウント有効化とパスワード復旧の最大の山場であるこのセクションでは、いよいよproduction (本番) 環境でアプリケーションからメールを送信します。最初に無料のサービスを利用してメールを送信し、続いてアプリケーションの設定とデプロイを行います。
production環境からメール送信するために、「SendGrid」というHerokuアドオンを使用してアカウントを検証します (このアドオンを使用するにはHerokuアカウントにクレジットカードを設定する必要がありますが、アカウント検証では料金は発生しません)。本チュートリアルでは、「starter tier」というサービスを使用することにします。これは、(執筆時点では) 1日の最大メール数が400通までという制限がありますが、無料で使用することができます。アドオンをアプリに追加するには、以下のコマンドを実行します。
$ heroku addons:create sendgrid:starter
(訳注: herokuコマンドのバージョンが古いとここで失敗するかもしれません。その場合は、Heroku Toolbelt ( https://toolbelt.heroku.com/ ) を使って最新版に更新するか、次の古い文法のコマンドを試してみてください: $ heroku addons:add sendgrid:starter )
アプリケーションでSendGridアドオンを使用するには、production環境のSMTPに情報を記入する必要があります。リスト10.56に示したとおり、本番Webサイトのアドレスをhost
変数に定義する必要もあります。
Rails.application.configure do
.
.
.
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :smtp
host = '【ここにHerokuのサブドメインを入力】.herokuapp.com'
config.action_mailer.default_url_options = { host: host }
ActionMailer::Base.smtp_settings = {
:address => 'smtp.sendgrid.net',
:port => '587',
:authentication => :plain,
:user_name => ENV['SENDGRID_USERNAME'],
:password => ENV['SENDGRID_PASSWORD'],
:domain => 'heroku.com',
:enable_starttls_auto => true
}
.
.
.
end
リスト10.56のメール設定にはSendGridアカウントのuser_name
とpassword
設定を記入する行もありますが、そこには記入せず、必ず環境変数「ENV
」に設定するよう十分ご注意ください。本番運用するアプリケーションでは、暗号化されていないIDやパスワードのような重要なセキュリティ情報は「絶対に」ソースコードに直接書き込まないでください。そのような情報は環境変数に記述し、そこからアプリケーションに読み込む必要があります。今回の場合、そうした変数はSendGridアドオンが自動的に設定してくれますが、11.4.4では環境変数を自分で設定しなければなりません。参考までに、リスト10.56で使用するHerokuの環境変数を表示するには、以下のコマンドを実行します。
$ heroku config:get SENDGRID_USERNAME
$ heroku config:get SENDGRID_PASSWORD
この時点で、Gitのトピックブランチをmasterにマージしておきましょう。
$ bundle exec rake test
$ git add -A
$ git commit -m "Add password resets & email configuration"
$ git checkout master
$ git merge account-activation-password-reset
続いてリモートリポジトリにプッシュし、Herokuにデプロイします。
$ bundle exec rake test
$ git push
$ git push heroku
$ heroku run rake db:migrate
Herokuへのデプロイが完了したら、自分が管理しているメールアドレスを使用して、production環境のサンプルアプリケーションでユーザー登録を行ってみましょう。10.1.1で実装した有効化メールが配信されるはずです (図10.20)。また、パスワードを忘れた時の再設定手順も10.2で実装したとおりに動作するはずです (図10.21)。
10.4 最後に
アカウント有効化機能とパスワード再設定機能が追加されたことで、ついにサンプルアプリケーションの登録、ログイン、ログアウト機能がすべて本格的に実装完了しました。Railsチュートリアルのこの後の章では、Twitterのようなマイクロポスト機能 (第11章) と、フォロー中のユーザーの投稿のステータスフィード機能 (第12章) の基本的な部分をサイトに搭載することにしましょう。それらの章では、Railsの強力な機能 (画像アップロード、カスタムのデータベースクエリ、has_many
やhas_many :through
を使用した高度なデータベースモデリングなど) を多数紹介する予定です。
10.4.1 本章のまとめ
- アカウント有効化は Active Recordオブジェクトではないが、セッションの場合と同様に、リソースでモデル化できる。
- メール送信のためのActive Mailerアクションやビューの生成機能がRailsに備わっている。
- Action MailerではテキストメールとHTMLメールを両方利用できる。
- メイラーアクションで定義したインスタンス変数は、他のアクションやビューと同様、メイラーのビューから参照できる。
- パスワードの再設定は Active Recordオブジェクトではないが、セッションやアカウント有効化の場合と同様に、リソースでモデル化できる。
- アカウント有効化やパスワード再設定では、ユーザーを有効化したりパスワードを再設定するために一意のURLを作成する。一意のURLには生成したトークンが使用される。
- メイラーのテストと統合テストは、どちらもUserメイラーの振舞いを確認するのに有用。
- SendGridを使用するとproduction環境からメールを送信できる。
10.5 演習
注: 『演習の解答マニュアル (英語)』にはRuby on Railsチュートリアルのすべての演習の解答が掲載されており、www.railstutorial.orgから購入いただいた方は無料で入手いただけます。
演習とチュートリアル本編の食い違いを避ける方法については、3.6のトピックブランチの演習に追加したメモをご覧ください。
-
リスト10.57のテンプレートを埋めて、期限切れのパスワード再設定のブランチ (リスト10.52) の統合テストを作成してください (10.57 のコードにある
response.body
は、そのページのHTML本文をすべて返すメソッドです)。期限切れのテスト方法はさまざまですが、リスト10.57でおすすめした手法 (大文字小文字は区別されません) を使えば、レスポンスの本文に「expired」という語があるかどうかをチェックできます。 - 現在は、/usersのユーザーインデックスページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト10.589のテンプレートに記入して、この動作を変更してください。なお、ここで使用するActive Recordの
where
メソッドについては、11.3.3でもう少し詳しく説明します。応用問題: /usersと/users/:id両方の統合テストを作成してください。 -
リスト10.42では、
activate
メソッドとcreate_reset_digest
メソッドの両方でupdate_attribute
を呼び出しており、それぞれのアクセスによってデータベーストランザクションが個別に発生してしまう点が残念です。リスト10.59のテンプレートに記入することで、個別のupdate_attribute
呼び出しを単一のupdate_columns
呼び出しに統合し、データベースアクセスが1回で済むようにしてください。変更後にテストを実行し、GREENになることを確認してください。
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:michael)
end
.
.
.
test "expired token" do
get new_password_reset_path
post password_resets_path, password_reset: { email: @user.email }
@user = assigns(:user)
@user.update_attribute(:reset_sent_at, 3.hours.ago)
patch password_reset_path(@user.reset_token),
email: @user.email,
user: { password: "foobar",
password_confirmation: "foobar" }
assert_response :redirect
follow_redirect!
assert_match /FILL_IN/i, response.body
end
end
class UsersController < ApplicationController
.
.
.
def index
@users = User.where(activated: FILL_IN).paginate(page: params[:page])
end
def show
@user = User.find(params[:id])
redirect_to root_url and return unless FILL_IN
end
.
.
.
end
update_columns
を使用するテンプレート app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
.
.
.
# アカウントを有効にする
def activate
update_columns(activated: FILL_IN, activated_at: FILL_IN)
end
# 有効化用のメールを送信する
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
# パスワード再設定の属性を設定する
def create_reset_digest
self.reset_token = User.new_token
update_columns(reset_digest: FILL_IN,
reset_sent_at: FILL_IN)
end
# パスワード再設定用メールを送信する
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
private
# メールアドレスをすべて小文字にする
def downcase_email
self.email = email.downcase
end
# 有効化トークンとダイジェストを作成および代入する
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
10.6 証明: 期限切れの比較
この説では、10.2.4で用いたパスワード期限切れの期間の比較が正しいことを証明します。最初に、期間を2つ定義します。\( \Delta t_r \)はパスワード再設定メールを送信してからの期間、\( \Delta t_e \)はパスワード再設定の有効な期間 (例: 2時間) と定めます。パスワードの再設定は、メールが送信された時刻から経過した期間が、有効期間よりも長くなった場合に「期限切れ」となります。これを次のように表します。
ここで、現在時刻 (訳注: 比較を行った時刻) を\( t_N \)で表し、パスワード再設定メールの送信時刻を\( t_r \)で表し、有効期間が切れる時刻を\( t_e \) (例: 2時間前) と表すと、次の2つの関係式を得ることができます。
および
式(10.2)と式(10.3)を(10.1)に代入すると、次の結果が得られます。
両辺に\( -1 \)をかけると、次の式が得られます。
(10.4)をRailsコードに置き換え、値を\( t_e = \mathrm{2\ 時間前} \)とすると、リスト10.53のpassword_reset_expired?
メソッドと同じコードになります。
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
10.2.4でも説明したとおり、「<
」を「〜より少ない」ではなく「〜より早い時刻」と解釈すれば、「パスワードの再設定は、現在より2時間以上前の時刻に行われた」という言明と一致します。
- 細かいことを言えば、9.1のアカウント設定更新機能を使って、おかしなメールアドレスを設定することもできてしまいます。ここで行う実装では、メールによるメールアドレス検証のメリットを (やりすぎない範囲で) 十分享受できるはずです。↑
- せっかくユーザーIDが既にアプリケーションのURLでそのまま使われているのですから、メールアドレスの代わりにユーザーIDを使うという手ももちろんあります。ただ、メールアドレスにしておけば、将来何らかの理由でユーザーIDをぼかしておきたくなる場合に役に立つかもしれません (競合他社があなたのアプリケーションのユーザー数を推測しにくいようにしたい、など)。↑
- こういう場合は
update
アクションにするのではないかとお思いの方もいるでしょう。しかし、有効化リンクはメールでユーザーに送られることを思い出してください。このリンクをクリックすれば、ブラウザで普通にクリックしたのと同じことになり、ブラウザから発行されるのは (update
アクションで使用するPATCHリクエストではなく) 必然的にGETリクエストになります。GETリクエストを受けるにはeditアクションにならざるを得ないわけです。↑ -
edit
アクションを使いたいのですから、コマンドラインでedit
と指定すればよいように思えますが、そうすると使いもしないeditビューやテストまで生成されてしまうのです。↑ - たとえば、攻撃者が仮にデータベースにアクセスできてしまうと、攻撃者が作成した新しいアカウントを即座に有効にすることができてしまいます。攻撃者はそのアカウントでゆうゆうとログインし、パスワードを変更してそのアカウントの権限を手に入れることでしょう。↑
- URLのクエリパラメータではキー/値ペアを複数使用することもできます。その場合は、「
&
」を使用して「/edit?name=Foo%20Bar&email=foo%40example.com
」のように区切ります。↑ - これについてもう少し詳しくお知りになりたい場合は、「ruby rails escape url」で検索してみてください。おそらく2通りの手法 (
URI::encode(str)
とCGI::escape(str)
) が見つかると思います。両方試してみるとわかると思いますが、実際に動作するのは後者の方です (実はもうひとつ方法があります:ERB::Util
ライブラリのurl_encodeメソッドでも同じことができます)。↑ - この場合、パスワードフィールドが空である場合だけを扱います。パスワードの確認フィールドが空の場合は、確認フィールドのバリデーションで検出され、エラーメッセージが表示されるので不要です。ただし、パスワードフィールドとパスワード確認フィールドが両方空だとバリデーションがスキップされてしまいます。↑
-
リスト10.58では、
&&
ではなくand
を使用していることにご注意ください。&&とandの動作は「ほぼ」同等ですが、&&演算子の方がandよりも優先順位が高いので、&&だとroot_url
との論理的な結び付きが強くなりすぎてしまい、不適切です。root_url
をかっこで囲んでこの問題を回避することもできますが、and
を使用する方が常道です。↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!