やること
アカウントを有効化するためのメール認証システム作成
新規登録したユーザーの機能を制限し、メールを用いた有効化を行うことで本当にそのアドレスの持ち主なのか確認できるようにする
- ユーザーの初期状態は有効化されていない
- 登録時に有効かトークンとそれに対応する有効化ダイジェストを生成
- 有効化ダイジェストはDBに保存し有効化トークンはアドレスと一緒にユーザーに送信する有効化用メールに仕込んでおく
- ユーザーがリンクをクリックしたら、アドレスをキーにユーザーを探し、DB内の有効化ダイジェストと比較することでトークンを認証する
- 認証できたら有効化する
用語
クエリパラメータ
URLの末尾で疑問符「?」に続けてキーと値のペアを記述したもの
エスケープ
通常URLでは扱えない文字を扱えるようにするために変換すること
11.1 AccountActivationsリソース
editアクションに変更していく
11.1.1AccountActivationsコントローラ
コントローラ生成
$ rails generate controller AccountActivations
リスト11.1 ルーティングにresources行追加
/account_activation/トークン/editにgetリクエストが送られるとaccount_activationsコントローラのeditアクションが実行される
名前付きルートのedit_account_activation_url(token)も使える
演習
2. メールのURLからアクセスするため
_pathは相対パス、_urlは絶対パス
11.1.2 AccountActivationのデータモデル
仮想的な属性を使ってハッシュ化した文字列をDBに保存する
$ rails generate migration add_activation_to_users
activation_digest:string activated:boolean activated_at:datetime
Userモデルにユーザー有効化用の3つの属性追加
リスト11.2 マイグレーションファイルのactivated属性のデフォルトをfalseにしておく
$ rails db:migrate
マイグレーション実行
Activationトークンのコールバック
ユーザーの新規登録完了には必ず有効化が必要になる
→有効化トークンと有効化ダイジェストはユーザーオブジェクトが作成される前に作っておく必要がある
→before_createコールバックを使う
before_create :create_activation_digest
create_activation_digestメソッドはUserモデル内でしか使わないので、privateキーワードを指定して隠蔽する
リスト11.3 アカウント有効化コードの追加
記憶トークンと記憶ダイジェストのために作ったrememberメソッドとの違い
記憶トークンと記憶ダイジェストはすでにDBにいるユーザーのために作成されるのでupdate_attributeで更新・保存される
create_activation_digestメソッドはbefore_createコールバックによりユーザー作成前に呼び出されるので更新ではなく新たに作成される
リスト11.4 サンプルユーザーを最初から有効にしておく
リスト11.5 fixtureのユーザーを有効にしておく
DBを初期化してサンプルデータを再生成
$ rails db:migrate:reset
$ rails db:seed
演習
3. def downcase_email
self.email.downcase!
end
11.2 アカウント有効化のメール送信
アカウント有効化メールの送信のコード追加
Action Mailerライブラリを使いUserのメイラー追加
メイラーはUsersコントローラのcreateアクションで有効化リンクを送るのに使う
メールのテンプレートはビューと同じ要領で定義
この中に有効化トークンと有効にするアカウントのメールアドレスのリンクを含める
11.2.1 送信メールのテンプレート
メイラーはモデルやコントローラと同様、rails generateで生成
$ rails generate mailer UserMailer account_activation password_reset
account_activationメソッドとpassword_resetメソッドが生成された
メイラーごとに2つずつ、ビューのテンプレートも生成された
リスト11.11 アプリケーションメイラーの送信元アドレスを設定
リスト11.12 account_activationメソッドでアカウント有効化リンクを送れるようにする
リスト11.13、11.14 アカウント有効化のテキストビュー
挨拶文にユーザー名を含め、有効化リンクを追加
リンクにはメールアドレスとトークンの両方含める
edit_account_activation_url(@user.activation_token, email: @user.email)
こうするとクエリパラメータが設定され特殊な文字も勝手にエスケープしてくれる
11.2.2 送信メールのプレビュー
メールを送信せずに確認できるメールプレビューを使うために
リスト11.16 development環境の更新
host = https以降のURL
config.action_mailer.default_url_options = { host: host, protocol: 'https' }
次はUserメイラーの更新をする
リスト11.18
userにDBの最初のユーザーを代入して、userのactivation_tokenに有効かトークンを代入
UserMailerクラスのaccount_activationメソッドの引数にuser渡し、呼び出す
11.2.3 送信メールのテスト
メールプレビューのテスト作成
リスト11.20 Userメイラーのテストの更新
assert_matchメソッドは正規表現で文字列をテストできる
これを使って名前、有効化トークン、エスケープ済みメールアドレスが本文に含まれているかテストする
CGI.escape(user.email)でテスト用ユーザーのメールアドレスをエスケープできる
パスワード設定のテストは削除している
リスト11.21 上記テストをパスさせるためテストのドメインホストを設定
11.2.4 ユーザーのcreateアクション更新
users_controllerのcreateアクション更新
リスト11.23 ユーザーが保存されたときアカウント有効化メールを送信し、flashメッセージを表示してルートURLに飛ばす
ログインしないよう変更
登録時の挙動が変更されたためテストが通らない
リスト11.24 該当箇所をコメントアウト
まだ実際にはメールが送信できない
演習
2 user = User.find(101)
user.activated?
11.3 アカウントを有効化する
AccountActivationコントローラのeditアクションを書いていく
テストも書き終えたらAccountActivationコントローラからUserモデルにコードを移していく
11.3.1 authenticated?メソッドの抽象化
有効化トークンとメールはそれぞれparams[:id]、params[:email]で参照できるので
次のようなコードでユーザーを検索して承認する
user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])
↑のauthenticated?メソッドはアカウント有効化アカウント有効かダイジェストと渡されたトークンが一致するかチェックするもの
しかし、authenticated?メソッドは記憶トークン用なので動作しない
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
↑のrememberの部分を変数として扱いたい
→sendメソッドを使う
渡されたオブジェクトにメッセージを送ることで呼び出すメソッドを決める
(受け取ったパラメータに応じて呼び出すメソッドを変えることをメタプログラミングという)
例)
$ rails console
a = [1, 2, 3]
a.length
=> 3
a.send(:length)
=> 3
a.send("length")
=> 3
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"
シンボルと文字列どちらでも良い
リスト11.26 書き換えたauthenticated?メソッド
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
このままではテストに失敗する
current_userメソッドとnilダイジェストのテストでauthenticated?が古いままのため
リスト11.28 current_user内のauthenticated?書き換え
リスト11.29 Userテスト内のauthenticated?書き換え
演習
1 有効化用のトークン、ダイジェストは存在するが、記憶トークン、ダイジェストはない
2 記憶トークンと記憶ダイジェストを生成
user.remember_token = User.new_token
user.update_attribute(:remember_digest, User.digest(user.remember_token))記憶トークンとダイジェストの認証
user.authenticated?(:activation, user.activation_token)有効化トークンとダイジェストの認証
11.3.2 editアクションの有効化
authenticated?メソッドが有効化トークン、ダイジェストにも対応するようになったのでeditアクションを書いていく
このアクションはparamsハッシュで渡されたメールアドレスに対応するユーザーを認証する
if user && !user.activated? && user.authenticated?(:activation, params[:id])
!user.activated?は既に有効になっているユーザーを再度有効にしないために必要
↑の論理値に基づいてユーザーを認証するには認証してからactivated_atタイムスタンプを更新する
リスト11.31 アカウントを有効にするeditアクション
リスト11.32 有効でないユーザーがログインしないようにする
sessions_controllerのcreateアクションの更新
演習
1 account_activationsとedit〜の間
11.3.3 有効化のテストとリファクタリング
リスト11.33 アカウント有効化の統合テスト追加
assert_equal 1, ActionMailer::Base.deliveries.size
↑のコードは配信されたメッセージが1つだけかどうか確認している
配列deliveriesは変数なので冒頭のsetupメソッド内で初期化している
assignsメソッドを使うと対応するアクション内のインスタンス変数にアクセスできるようになる。Usersコントローラのcreateアクションでは@userというインスタンス変数が定義されているがテストでassigns(:user)と書くとアクセスできるようになる
ここからリファクタリング
リスト11.35 Userモデルにユーザー有効化メソッド追加
リスト11.36,37 Userモデルに追加したメソッドを使って書き換え
演習
1 update_columns(activated: true, activated_at: Time.zone.now)
2 def index
@users = User.where(activated: true).paginate(page: params[:page])
end
def show
@user = User.find(params[:id])
redirect_to root_url and return unless @user.activated?
end
3 fixtureに有効化されていないユーザー追加
users_controller_test内にテスト追加
非有効化ユーザーでログイン
有効化されていないことを確認
/usersを取得
非有効化ユーザーが表示されていないことを確認
非有効化ユーザーのidを直接取得
ルートURLにリダイレクトされたらtrue
11.4 本番環境でのメール送信
本番環境でメールを送信するために「Mailgun」というherokuアドオンを使用(クレカ登録必須)
starterという無料プラン
リスト11.41 Mailgunを使う設定
<あなたのHerokuサブドメイン名>にはhttpsから.herokuapp.comの前まで入れる
user_nameとpasswordの記入はせず必ず環境変数ENVに設定する
重要な情報は絶対にソースコードに書き込まない
git操作
$ rails test
$ git add -A
$ git commit -m "Add account activation"
$ git checkout master
$ git merge account-activation
$ rails test
$ git push
$ git push heroku
$ heroku run rails db:migrate
MailgunのHerokuアドオン追加
$ heroku addons:create mailgun:starter
受信メールの認証を行う
$ heroku addons:open mailgun
画面左側Sending → Domains → sandbox
Authorized Recipientsから受信アドレスを認証して準備完了
push後に3箇所ほどスペルミスがあったため修正に苦戦した
よく見直そう