kouの技術的メモ

学習した内容の定着やアウトプット用に開設しました

Railsチュートリアル 第11章 アカウントの有効化

今回は 登録されたメールアドレスが、本当にそのメールアドレスが本人なのか確認する為に、 アカウントを有効化するステップを新規登録の途中に差し込むことで、本当にそのメールアドレスの持ち主なのかどうかを確認できるようします。 よくwebサービスにある新規アカウントで仮登録すると、その登録したメールアドレスにメールが送られてきて、 メールに書かれているリンク経由を踏むと本登録される、あれですね。

大まかな流れは、

  1. 有効化トークンやダイジェストを関連付けておいた状態で、

  2. 有効化トークンを含めたリンクをユーザーにメールで送信し(createアクションで有効化リンクをメール送信)、

  3. ユーザーがそのリンクをクリックすると有効化できるようにする、というものです。

11.1.1 AccountActivationsコントローラ

リスト 11.1: アカウント有効化に使うリソース (editアクション) を追加する 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'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
  resources :account_activations, only: [:edit]          #ここを追加する
end

アカウント有効化用のRESTfulなルーティング設定 | メソッド | URL | Action | 名前付きルート | |:-----------|------------:|:------------:|----------:| |GET|account_activation//edit |edit |edit_account_activation_url(token)|

11.2 アカウント有効化のメール送信

メールを送る部分のコードを書いていきます。具体的にはAction Mailerライブラリを使ってUserのメイラー(メールを送る機能部品)を追加します。 createアクションで有効化リンクをメール送信します。 メイラーの構成はコントローラのアクションとよく似ており、メールのテンプレートをビューと同じ要領で定義できます。このテンプレートの中に有効化トークンとメールアドレス (= 有効にするアカウントのアドレス) のリンクを含め、使っていきます。

リスト 11.6: Userメイラーの生成 メイラーはコントローラと同じようにrails generateで生成します。

$ rails generate mailer UserMailer account_activation password_reset

メイラーのビューファイル(つまり実際のメールのぶん面)はapp/views/user_mailer/にあり、

テキスト形式メールのaccount_activation.text.erbと、html形式メールのaccount_activation.html.erbがあります。

また、実際にメール送信機能を担う部品は

  • Applicationメイラーapp/mailers/application_mailer.rb(送信元メールアドレスfromなどの情報がある)

  • Userメイラーapp/mailers/user_mailer.rb(送信先を指定したり、どういう時にどういうメールを送りたいか、などを指定する。) Userコントローラーに近い機能。

になります。

リスト 11.11: fromアドレスのデフォルト値を更新したアプリケーションメイラー app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  default from: "noreply@example.com"
  layout 'mailer'
end

リスト 11.12: アカウント有効化リンクをメール送信する app/mailers/user_mailer.rb

 class UserMailer < ApplicationMailer

  def account_activation(user)
    @user = user                                 #ユーザー情報でインスタンス変数を生成
    mail to: user.email, subject: "Account activation"               #登録されたuser.emailでメール送信。subjectはメールの件名の部分。
  end

  def password_reset
    @greeting = "Hi"

    mail to: "to@example.org"
  end
end

メールに載せておくべきリンクには、トークン情報と、仮登録されたユーザーを特定するためにメールアドレス情報を載せておきたい つまりこういうURLとなる

http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com

上のURLのコーディング上での名前付きルートは

edit_account_activation_url(@user.activation_token, email: @user.email)

となる(@user.activation_token)はUserモデルで定義したハッシュ化前の有効化トークン

メールアドレス部分は?以降の部分として含める
?email=foo%40example.com

上記の?以降のメールアドレス部分はクエリパラメータと言って、キーと値のペアを記述したものです。 つまり、

  • キー :email

  • 値 : foo%40example.comつまり、foo@example.com(@部分はurlに含めることができないのでエスケープトイウ手法で%40として書いている。)

となり、キーと値で、データベースから、仮登録情報を検索できる。

上記の事から、メールにhttp://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.comのリンクを貼るには ビューファイルに

リスト 11.13: アカウント有効化のテキストビュー app/views/user_mailer/account_activation.text.erb

 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) %>
リスト 11.14: アカウント有効化のHTMLビュー

app/views/user_mailer/account_activation.html.erb

<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) %>

と書けば良いことになります。

11.2.2 送信メールのプレビュー

送信メールがどう表示されるか簡単に確認するために、メールプレビューと言う裏技があります。 Railsでは、特殊なURLにアクセスするとメールのメッセージをその場でプレビューすることができます。

そのための設定でが、少し注意が必要です。

リスト 11.16: development環境のメール設定 config/environments/development.rb

 Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = '〇〇〇〇.amazonaws.comなどの自分の環境'          # ここを自分の環境に設定する。テストプレビュー時のメール内リンク先にここが設定される。
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  .
  .
  .
end

host=のところに自分のdevelop環境を設定します。 awsなら〇〇〇〇.amazonaws.com等(rails serverを実行したときの開発環境の基本URL)を貼り付けると、テストプレビュー時のメール内リンク先にここが設定されます。

11.2.3 送信メールのテスト

メイラーのテストファイルがtest/mailers/user_mailer_test.rbにganerate機能により自動生成されています。

ここではassert_matchという正規表現の文字列も使えるメソッドが使えます。

正規表現 正規表現とは…

  • いくつかの文字列を一つの形式で表現するための表現方法

  • たくさんの文章の中から容易に見つけたい文字列を検索することができるためよく利用される。

正規表現 意味
\w アルファベット、数字、アンダスコア( _ )のどれかの1文字
+ 直前の文字の1回以上の繰り返し
  • assert_match   'foo',   'foobar'    # true

  • assert_match   'baz',   'foobar'   # false

  • assert_match   /\w+/,   'foobar'   # true

  • assert_match   /\w+/,   '$#!*+@'   # false

11.3.1 authenticated?メソッドの抽象化

ユーザーにメールリンクにアクセスしてもらって、そのデータを受け取ってアクティベーションを行うコントローラ部分の実装に入ります。 有効化トークンとメールをそれぞれparams[:id]とparams[:email]で参照するので、

user.rbの def current_user が使う,authenticated?メソッドを再利用できるかもしれません。

 # 記憶トークンcookieに対応するユーザーを返す。
(remember機能のcookie[remember_token]とuser.digestユーザー情報を判定する機能部品)
  def current_user     
.
.
.

    elsif (user_id = cookies.signed[:user_id])                                  #もしセッションIDが無ければ、永続化クッキーを解読してを参照し、値が存在すれば
      user = User.find_by(id: user_id)                                          #userに永続化クッキーのuser_idを代入
      if user && user.authenticated?(cookies[:remember_token])                  #もしそのクッキーidのユーザーがいて、ブラウザクッキーとDB上remember_digestが一致したら

上の判定式で使う authenticated?メソッド

# トークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
  return false if remember_digest.nil?
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

ただ、ただし、このメソッドは記憶トークン用(remember me機能用)なのでそのままでは使い回せません。

remember_digest

の部分を

self.FOOBAR_digest

変数(FOOBAR部分)として扱えるようにすると、モデルからremember_digestとactivation_digestとを切り替えたい。 それを実現するためにauthenticated?メソッドを改良して、受け取ったパラメータに応じて呼び出すメソッドを切り替える手法を使います。 この一見不思議な手法は「メタプログラミング」と呼ばれています。 メタプログラミングを一言で言うと「プログラムでプログラムを作成する」ことです。 この強力な機能によって、rails自体の機能の多くは作られており、sendメソッドを使って実現しています sendメソッドは渡されたオブジェクトに「メッセージを送る」ことによって、呼び出すメソッドを動的に決めることができます。

$ rails console
>> a = [1, 2, 3]
>> a.length
=> 3
>> a.send(:length)
=> 3
>> a.send("length")
=> 3

上記の例はオブジェクトにlengthメソッドを渡して、メソッドの結果を得ることができる。

書き方は
オブジェクト.send( :メソッド名 or "メソッド名")

これを利用して、〇〇○digestの〇〇○部分をrememberか、activationかを状況に応じて変える(変数化する)には

>> 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     #文字列’activation’でも同じことができますが、Rubyではシンボルが一般的
>> user.send("#{attribute}_digest")        
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
上記3つは全てUserのactivation_digestメソッドを実行すると言う、同じ意味を持った書き方。

とすればいいことになります。

つまり、暗号化されたdigestとトークンが一致するか判定するauthentcated?メソッドをrememberとactivation両方で使えるようにするには 引数をもう2つに増やし(トークンと、rememberとactivationどっちかを選ぶ)

def authenticated?(attribute, token)
  digest = self.send("#{attribute}_digest")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token)
end

と書けば良いことになります。

そして、この新しいauthenticated?メソッドを使う側メソッドにattribute用の引数を追加してあげます

11.3.2 editアクションで有効化

メールのactivationトークンを比較判定するメソッドが完成したので、 いよいよそれを使ってメールのリンクが押された時に本登録される、認証部分の核となるeditアクションを制作していきます。

if user && !user.activated? && user.authenticated?(:activation, params[:id])

ここでの!user.activated?はuserモデルのactivatedの中身がfalseならtrueを返す式です。つまり本登録が住んでいるかどうかをここで判定しています。 Userテーブルのactivatedカラム(boorean値)の中身を判定してくれる.activated?を使っています

以下が判定、ログインできる文になります

  def edit                                                                      #メール本文の認証リンクが押されたらこれが実行される
    user = User.find_by(email: params[:email])                                  #リンクのメールアドレスからid検索
    if user && !user.activated? && user.authenticated?(:activation, params[:id])#ユーザーが実在し、activateカラムがまだfalseで、activation_digestとメールのトークンが一致したら
      user.update_attribute(:activated,    true)                                #activateカラムをtrueにする
      user.update_attribute(:activated_at, Time.zone.now)                       #activated_atカラムに現時点の時間を入れる
      log_in user                                                               # session[:user_id]にユーザーidを入れ、ログイン
      flash[:success] = "Account activated!"                                    #成功フラッシュ
      redirect_to user                                                          #/users/idにリダイレクト
    else
      flash[:danger] = "Invalid activation link"                                #失敗フラッシュ
      redirect_to root_url                                                      #ルートページにリダイレクト 
    end
  end
end

後は有効ではないユーザーがログインすることのないようにsessions_controller.rbを書き換え、

11.3.3 有効化のテストとリファクタリングでテストを作り、 activate機能とメール送信機能をuser.rb内にメソッド化、リファクタリング

  # アカウントを有効にする
  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
11.4 本番環境でのメール送信

production(本番)環境で実際にメールを送信できるよう、「SendGrid」というHerokuアドオンを設定していきます。

heroku addons:create sendgrid:starter

でインストールしたら、以下を追記します

リスト 11.41: Railsのproduction環境でSendGridを使う設定 config/environments/production.rb

 Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  host = '<your heroku app>.herokuapp.com'          #ここを自分のHerokuアプリのurlに変えることを忘れずに
  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

host = '.herokuapp.com' を自分のアプリのURLに帰ることを忘れずにしましょう。 後は普通にgitコマンド、herokuコマンドを打って完成です。


ここで問題が発生しました。 実際にアカウント登録してみても、メールが送られてこないのです。


コード自体は問題ないようで、heroku logsを見てもサーバー側でメールはきちんと作成され、sendできているようなので、
後はheroku、railsとSendGridの連帯の関係かなと思って原因を調べていった結果、


なんとSndGridのwebポータルサイトでActivityを調べたところsendgridアカウントが一時的に凍結になってました。。
どうもSendGrid社のメールのフィルタリング機能で引っかかってしまったようで、こういうケースはたまにあるらしいです。


とりあえず公式ヘルプでこういう場合はsendgridの米国本社の方に連絡してくれと書いてあったので、 英語の文面で、起こった問題と現状と、解決してほしい送っておきました。
次回の12章パスワードの再設定でもメール機能は使うかもしれないので、ぜひ解決したいですね。



追記
多少時間はかかりましたが、SendGrid社の担当の方とメールで数通のやり取りした後、無事にアカウント登録のメール送信機能が使えるようにになりました。 やはり設定やコードの方に問題はなかったようです。 

無料で使わせて頂いているにも関わらず、素早いレスポンスと、ご丁寧な対応をしていただいてSendGrid社の方には本当に感謝しています。


11章まとめ
  • アカウント有効化は Active Recordオブジェクトではないが、セッションの場合と同様に、リソースでモデル化できる

  • Railsは、メール送信で扱うAction Mailerのアクションとビューを生成することができる

  • Action MailerではテキストメールとHTMLメールの両方を利用できる

  • メイラーアクションで定義したインスタンス変数は、他のアクションやビューと同様、メイラーのビューから参照できる

  • アカウントを有効化させるために、生成したトークンを使って一意のURLを作る

  • より安全なアカウント有効化のために、ハッシュ化したトークン (ダイジェスト) を使う

  • メイラーのテストと統合テストは、どちらもUserメイラーの振舞いを確認するのに有用

  • SendGridを使うと、production環境からメールを送信できる

  • 難易度の高いエラーが出ても、課題解決方法を模索すれば結構なんとかなる