前提
- アカウントの有効化機能は、セッション機能と同様、RESTfulなリソースとしてモデル化する
- AccountActivationsコントローラーの実装は、Sessionsコントローラーの実装と似た形になる
- ただし、以下のユースケースから、ここまで実装してきたリソースとは異なる形の実装となる
- 「有効化リンクはメールでユーザーに送信される。ユーザーが当該リンクをクリックすることにより、アプリケーションでアカウントの有効化プロセスが開始される」というユースケースである
- Webブラウザでリンクをクリックした場合と同様、このときユーザーから送られるのは
GET
リクエストである -
GET
リクエストが直接のトリガーになるゆえ、「edit
アクションで直接RDBの内容が変更される」という実装になる
- Webブラウザでリンクをクリックした場合と同様、このときユーザーから送られるのは
AccountActivationsコントローラー
AccountActivationsコントローラーの生成
まずはrails generate controller
コマンドにより、AccountActivationsコントローラーを生成するところから始まります。この部分は、Sessionsコントローラーと同じ手順ですね。
# rails generate controller AccountActivations
Running via Spring preloader in process 13245
create app/controllers/account_activations_controller.rb
invoke erb
create app/views/account_activations
invoke test_unit
create test/controllers/account_activations_controller_test.rb
invoke helper
create app/helpers/account_activations_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/account_activations.coffee
invoke scss
create app/assets/stylesheets/account_activations.scss
AccountActivationsリソースに関係するルーティング
AccountActivationsリソースのユースケースは、「ユーザーがリンクをクリックすることにより、アプリケーションでアカウントの有効化プロセスが開始される」というものです。当然ながら、「アカウントの有効化プロセスを開始するためのリンク」が必要になってきます。さらに、「RESTfulなリソースとしてモデル化する」という前提があります。というわけで、「アカウントの有効化プロセスを開始するためのリンク」は、「AccountActivationsリソースのedit
アクションに紐付けされたリンク」ということになります。
edit_account_activation_url(activation_token, ...)
AccountActivationsリソースのユースケースは、「ユーザーがリンクをクリックすることにより、アプリケーションでアカウントの有効化プロセスが開始される」というものに限られます。(edit
アクションではなく)リソースそのものへのGET
や、(Usersリソースとセットでない)単独でのPOST
・DELETE
、UPDATE
、以上のリクエストが送られてくることは想定されません。ゆえに、必要となるルーティングはedit
のみに限られます。
resources :account_activations, only: [:edit]
結果、Railsのルーティング(config/routes.rb
)は、以下のように変更する必要があります。
Rails.application.routes.draw do
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
post '/signup', to: 'users#create'
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
resources :users
+ resources :account_activations, only: [:edit]
end
演習 - AccountActivationsコントローラー
1. 現時点でテストスイートを実行するとgreen
になることを確認してみましょう。
# rails test
Running via Spring preloader in process 13277
Started with run options --seed 33183
43/43: [=================================] 100% Time: 00:00:15, Time: 00:00:15
Finished in 15.05852s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips
確かにテストは成功しますね。
2. 表 11.2の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。
ヒント: 私達はこれからメールで名前付きルートを使います。
「AccountActivationsリソースのedit
アクションに対応するURLは、メールに記載するURLとして使われる」というのが前提となる、というのが大きなポイントです。
- AccountActivationsリソースの
edit
アクションには、Railsアプリケーションの外部からアクセスできなければならない - Railsアプリケーションの外部からリソースにアクセスする場合、完全なURLが必要となる
このあたりが理由でしょうか。
AccountActivationのデータモデル
RDBには、有効化トークンをハッシュ化したものを保存する
「生の有効化トークンをRDBに保存する」という運用は、万が一RDBそのものの内容が漏洩した場合、容易に攻撃に悪用される脆弱な運用です。例えば、「攻撃者が新しく登録されたユーザーの有効化トークンを盗み取り、本来のユーザーが使う前に当該トークンを使ってしまう(そして当該ユーザーとしてログインしてしまう)」という攻撃が行われる危険性が想定できます。
というわけで、パスワードや記憶トークンと同様、有効化トークンについても、「RDBに保存するのはハッシュ化した値」という運用を行うこととします。
実装手法
実装手法は、節6.3におけるpassword
仮想属性の実装、ならびに、節9.1におけるremember_token
仮想属性の実装と類似しています。今回実装するのは、activation_token
という仮想属性となります。
user.activation_token
最終的には、以下のようなコードでユーザーの有効化トークンを認証できるようになることを目指します。
user.authenticated?(:activation, token)
なお、authenticated?
メソッドそのものの実装にも手を加えていくことになります。
Userモデルの実装内容を変更する
Userモデルには、以下3つの属性を新たに追加していくことになります。
-
activation_digest
- string型
- 有効化トークンに対するダイジェスト
-
activated
- boolean型
- ユーザーが有効化されたか否か
- デフォルトでは
false
である
-
activated_at
属性- datetime型
- ユーザーが有効化された日時
上記属性追加を反映した新たなUserモデルの全体像は、以下のようになります。
新たなUserモデルに対応するマイグレーション
まずはマイグレーションそのものを生成します。
root@705320d4d96d:/var/www/sample_app# rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
Running via Spring preloader in process 13329
invoke active_record
create db/migrate/[timestamp]_add_activation_to_users.rb
activated
属性のデフォルト値をfalse
とするため、生成されたマイグレーションを以下のように書き換えていきます。
class AddActivationToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :activation_digest, :string
- add_column :users, :activated, :boolean
+ add_column :users, :activated, :boolean, default: false
add_column :users, :activated_at, :datetime
end
end
最後はrails db:migrate
です。
# rails db:migrate
== [timestamp] AddActivationToUsers: migrating =============================
-- add_column(:users, :activation_digest, :string)
-> 0.0169s
-- add_column(:users, :activated, :boolean, {:default=>false})
-> 0.0015s
-- add_column(:users, :activated_at, :datetime)
-> 0.0018s
== [timestamp] AddActivationToUsers: migrated (0.0205s) ====================
Active Recordのコールバックメソッドにより、ユーザーオブジェクト作成前に有効化トークン・有効化ダイジェストが生成されるようにする
before_create
コールバック
「オブジェクトの新規作成時にのみ呼び出される」というコールバックです。引数として、メソッド名を表すシンボルを指定(あるいは実行する処理の内容をブロックとして直接記述)します。
今回は、「有効化トークン・有効化ダイジェストを生成する処理(create_activation_digest
メソッドとします)」をbefore_create
コールバックの対象とします。
before_create :create_activation_digest
create_activation_digest
メソッド
有効化トークンと、有効化トークンに対応するダイジェストを生成する処理の実体を記述するメソッドです。
private
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
private
メソッドとの関係
create_activation_digest
の前にprivate
メソッドが呼び出されているのは、一つの大きなポイントです。Userモデルのbeforeフィルターで呼び出されるメソッドは、Userモデル内でしか使わないので、User
クラス内でprivate
メソッドを呼び出して以降に記述すべきとされています。そういえば、UsersコントローラーにStrong Parametersやbeforeフィルターを実装した際にもprivate
メソッドを使いましたよね。
有効化トークンと、有効化トークンに対応するダイジェストを生成する処理の実体
表題記載の処理の実体は、以下2つの処理となります。
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
User#remember
メソッドとの類似点と相違点
類似する処理として、永続cookiesに関する記憶トークンと記憶ダイジェストを生成するUser#remember
メソッドを、Railsチュートリアル本文の第9章で定義していました。
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
User#new_token
メソッドの使い方は、remember
でもcreate_activation_digest
でも同じです。
一方、User#digest
メソッドの使い方は、remember
とcreate_activation_digest
で異なります。
-
remember
におけるUser#digest
メソッドは、update_attribute
メソッドの引数として呼び出している- すでにRDB上に存在するユーザー情報を対象とするため
-
create_activation_digest
におけるUser#digest
メソッドは、実行結果をself.activation_digest
に代入している- まだRDB上に存在しないユーザー情報を対象とするため
- RDB上にユーザー情報が生成されるのは、
create_activation_digest
が呼び出された後である
Userモデルへの実装の追加・変更の全体像
Userモデルへの実装の追加・変更の全体像は、以下のようになります。なお、既存の「メールアドレスをすべて小文字にする」という処理も、downcase_email
というメソッドを呼び出す実装に変更しています。
class User < ApplicationRecord
- attr_accessor :remember_token,
+ attr_accessor :remember_token, :activation_token
- before_save { email.downcase! }
+ before_save :downcase_email
+ before_create :create_activation_digest
...略
+
+ 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
この時点でテストは全て成功するはず
特に「RDBに格納されるデータにおいて、メールアドレスが全て小文字であるか」というテストが成功することは、再度確認が必要です。
test "email addresses should be saved as lower-case" do
# rails test test/models/user_test.rb:57
Running via Spring preloader in process 13346
Started with run options --seed 25681
12/12: [=================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.20554s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
「RDBに格納されるデータにおいて、メールアドレスが全て小文字であるか」というテストは、無事成功しました。
続いて全体のテストを実行してみましょう。
# rails test
Running via Spring preloader in process 13361
Started with run options --seed 7529
43/43: [=================================] 100% Time: 00:00:06, Time: 00:00:06
Finished in 7.00017s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips
こちらも無事成功しました。
サンプルユーザーの生成とテスト
サンプルユーザーを最初から有効にしておく
User.create(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
- admin: true)
+ 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)
+ password_confirmation: password,
+ activated: true,
+ activated_at: Time.zone.now)
end
fixtureのユーザーを最初から有効にしておく
rhakurei:
name: Reimu Hakurei
email: rhakurei@example.com
password_digest: <%= User.digest('password') %>
admin: true
+ activated: true
+ activated_at: Time.zone.now
mkirisame:
name: Marisa Kirisame
email: example.example@example.org
password_digest: <%= User.digest('password') %>
+ activated: true
+ activated_at: Time.zone.now
skomeiji:
name: Satori Komeiji
email: example_example@example.net
password_digest: <%= User.digest('password') %>
+ activated: true
+ activated_at: Time.zone.now
rusami:
name: Renko Usami
email: example0@example.com
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 %>
サンプルデータを再度生成し直す
# rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
...略
== [timestamp] AddActivationToUsers: migrated (0.0165s) ====================
# rails db:seed
#
「サンプルデータの入力におけるtypo」というのは案外やってしまいがちなので、この場面は結構緊張する場面です。今回は何事もなく完了しました。
演習 - AccountActivationのデータモデル
1. 本項での変更を加えた後、テストスイートがgreen
のままになっていることを確認してみましょう。
# rails test
Running via Spring preloader in process 13392
Started with run options --seed 56186
43/43: [=================================] 100% Time: 00:00:04, Time: 00:00:04
Finished in 4.90673s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips
特にfixtureの変更時におけるtypoには注意が必要です。
2.1. コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digest
メソッドを呼び出そうとすると (Privateメソッドなので) NoMethodError
が発生することを確認してみましょう。
# rails console --sandbox
>> user = User.first
>> user.create_activation_digest
Traceback (most recent call last):
1: from (irb):2
NoMethodError (private method `create_activation_digest' called for #<User:0x00007fdd8823ae98>)
Did you mean? restore_activation_digest!
2.2. また、そのUserオブジェクトからダイジェストの値も確認してみましょう。
>> user.activation_digest
=> "$2a$10$Q.bVywQrrgEJC6Mg0IhdXONY5M/0jQYm4/ZEBwhJfxcag7VBBx6S6"
3.1. downcase!
メソッドを使って、リスト 11.3のdowncase_email
メソッドを改良してみてください。
リスト 6.34で、メールアドレスの小文字化には
email.downcase!
という (代入せずに済む) メソッドがあることを知りました。
def downcase_email
- self.email = email.downcase
+ self.email.downcase!
end
3.2. また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。
# rails test test/models/user_test.rb:57
Running via Spring preloader in process 13416
Started with run options --seed 43627
12/12: [=================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.23544s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
# rails test
Running via Spring preloader in process 13429
Started with run options --seed 15928
43/43: [=================================] 100% Time: 00:00:04, Time: 00:00:04
Finished in 4.22996s
43 tests, 183 assertions, 0 failures, 0 errors, 0 skips