0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-11-19

#アカウントの有効化
 現時点のアプリケーションは、新規登録したユーザーは初めからすべての機能にアクセスできるようになっている (第7章)。本章では、アカウントを有効化するステップを新規登録の途中に差し込むことで、本当にそのメールアドレスの持ち主なのかどうかを確認できるようにする。
アカウントを有効化する段取りは、次のようになる。

1.ユーザーの初期状態は「有効化されていない」(unactivated) にしておく。

2.ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する。

3.有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく。

4.ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、メールのリンクに含まれる有効化トークンを、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。

5.ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」(activated) に変更す

##AccountActivationsリソース
セッション機能を使って、アカウントの有効化という作業を「リソース」としてモデル化する。この作業に必要なデータ (有効化トークンや有効化ステータスなど) をUserモデルに追加する。
 なお、アカウント有効化もリソースとして扱いたいが、いつもとは少し使い方が異なる点に注意する。例えば、有効化用のリンクにアクセスして有効化のステータスを変更する部分では、RESTのルールに従うとPATCHリクエストとupdateアクションになるべきである。しかし、有効化リンクはメールでユーザーに送られることを思い出そう。ユーザーがこのリンクをクリックすれば、それはブラウザで普通にクリックしたときと同じであり、その場合ブラウザから発行されるのは (updateアクションで使うPATCHリクエストではなく) GETリクエストになってしまう。このため、ユーザーからのGETリクエストを受けるために、(本来であればupdateのところを) editアクションに変更して使っていく。
###AccountActivationsコントローラ
 UsersリソースやSessionsリソースのときと同様に、AccountActivationsリソースを作るために、まずはAccountActivationsコントローラを生成する。

$ rails generate controller AccountActivations

有効化のメールには次のURLを含める。

edit_account_activation_url(activation_token, ...)

これは、editアクションへの名前付きルートが必要になるということである。そこでまずは、名前付きルートを扱えるようにするため、ルーティングにアカウント有効化用のresources行を追加する。

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

次にアカウント有効化用のデータモデルとメイラーを作っていくが、それが終わったらここで作ったリソースをもとにeditアクションを定義していく。

###AccountActivationのデータモデル
効化のメールには一意の有効化トークンが必要なので、パスワードの実装や記憶トークンの実装と同じように仮想的な属性を使ってハッシュ化した文字列をデータベースに保存するようにする。具体的には、次のように仮想属性の有効化トークンにアクセスし、

user.activation_token

このようなコードでユーザーを認証できるようになる。

user.authenticated?(:activation, token)

続いて、activated属性を追加して論理値を取るようにする。これでユーザーが有効であるかどうかをテストできるようになる。

if user.activated? ...

最後に、使うことはないが、ユーザーを有効にしたときの日時も念のために記録しておく。変更後のデータモデルは下の図のようになる。
image.png
次のマイグレーションをコマンドラインで実行して図のデータモデルを追加すると、3つの属性が新しく追加される。

$ rails generate migration add_activation_to_users \
> activation_digest:string activated:boolean activated_at:datetime

activated属性のデフォルトの論理値をfalseにしておく。

db/migrate/[timestamp]_add_activation_to_users.rb
class AddActivationToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :activation_digest, :string
    add_column :users, :activated, :boolean, default: false
    add_column :users, :activated_at, :datetime
  end
end

いつものようにマイグレーションを実行する。

$ rails db:migrate

 ユーザーが新しい登録を完了するためには必ずアカウントの有効化が必要になるので、有効化トークンや有効化ダイジェストはユーザーオブジェクトが作成される前に作成しておく必要がある。そこでbefore_createコールバック(オブジェクトが作成される前に呼び出されるメソッド)が必要になり、このコールバックは次のように定義できる。

before_create :create_activation_digest

create_activation_digestメソッド自体はUserモデル内でしか使わないので、外部に公開する必要はない。privateキーワードを指定して、このメソッドをRuby流に隠蔽する。

private

  def create_activation_digest
    # 有効化トークンとダイジェストを作成および代入する
  end

クラス内でprivateキーワードより下に記述したメソッドは自動的に非公開となる。
今回before_createコールバックを使う目的は、トークンとそれに対応するダイジェストを割り当てるためで、実際の割り当ては次のように行う。

self.activation_token  = User.new_token
self.activation_digest = User.digest(activation_token)

上で説明したことをUserモデルに実装すると下のようになる。有効化トークンは本質的に仮のものでなければならないので、このモデルのattr_accessorにもう1つ追加した。

app/models/user.rb
class User < ApplicationRecord
  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

先に進む前に、サンプルデータとfixtureも更新し、テスト時のサンプルとユーザーを事前に有効化しておく。

db/seeds.rb
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

データベースを初期化して、サンプルデータを再度生成し直し、上の変更を反映する。

$ rails db:migrate:reset
$ rails db:seed

##アカウント有効化のメール送信
データのモデル化が終わったので、今度はアカウント有効化メールの送信に必要なコードを追加する。このメソッドではAction Mailerライブラリを使ってUserのメイラーを追加する。このメイラーはUsersコントローラのcreateアクションで有効化リンクをメール送信するために使う。

###送信メールのテンプレート
メイラーは、モデルやコントローラと同様にrails generateで生成できる。

$ rails generate mailer UserMailer account_activation password_reset

これで今回必要となるaccount_activationメソッドと、第12章で必要となるpassword_resetメソッドが生成された。
また、このコードにより、生成したメイラーごとに、ビューのテンプレートが2つずつ生成される。1つはテキストメール用のテンプレート、1つはHTMLメール用のテンプレートである。
アカウント有効化に使うテンプレートを以下に示す。

app/views/user_mailer/account_activation.text.erb
UserMailer#account_activation

<%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erb
app/views/user_mailer/account_activation.html.erb
<h1>UserMailer#account_activation</h1>

<p>
  <%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb
</p>

生成されたメイラーの動作を簡単に追ってみよう。Applicationメイラーにはデフォルトのfromアドレス (アプリケーション全体で共通) があり、Userメイラーの各メソッドには宛先メールアドレスもある。生成されたコードにはインスタンス変数@greetingも含まれており、このインスタンス変数は、ちょうど普通のビューでコントローラのインスタンス変数を利用できるのと同じように、メイラービューで利用できる。
Applicationメイラー

app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout 'mailer'
end

Userメイラー

app/mailers/user_mailer.rb
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

最初に、生成されたテンプレートをカスタマイズして、実際に有効化メールで使えるようにする (リスト 11.11)。次に、ユーザーを含むインスタンス変数を作成してビューで使えるようにし、user.emailにメール送信する(リスト 11.12)。リスト 11.12では、mailにsubjectキーを引数として渡している。この値は、メールの件名にあたる。
リスト 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"
  end

  def password_reset
    @greeting = "Hi"

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

テンプレートビューは、通常のビューと同様ERBで自由にカスタマイズできる。ここでは挨拶文にユーザー名を含め、カスタムの有効化リンクを追加する。この後、Railsサーバーでユーザーをメールアドレスで検索して有効化トークンを認証できるようにしたいので、リンクにはメールアドレスとトークンを両方含めておく必要がある。これには名前つきルートに対して次のようにハッシュを追加し、リンクのURLにクエリパラメータとしてメールアドレスを組み込む。
なお、クエリパラメータとは、URLの末尾で疑問符「?」に続けてキーと値のペアを記述したものである。

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

こうすることで次のようなURLが生成される

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

上のURLの「q5lt38hQDc_959PVoo6b7A」という部分はnew_tokenメソッドで生成されたものであり、これはちょうど/users/1/editの「1」のようなユーザーIDと同じ役割を果たす。このトークンは、特にAccountActivationsコントローラのeditアクションではparamsハッシュでparams[:id]として参照できる。(メールアドレスの「@」記号がURLでは「%40」となっている点に注目。これは「エスケープ」と呼ばれる手法で、通常URLでは扱えない文字を扱えるようにするために変換されている。)
参考

edit_user_url(user)

このコードで

http://www.example.com/users/1/edit

のURLが生成される。
以上で、@userインスタンス変数、editへの名前付きルート、ERBを組み合わせて、必要なリンクを作成できる。

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

###ユーザーのcreateアクションを更新
あとはユーザー登録を行うcreateアクションに数行追加するだけで、メイラーをアプリケーションで実際に使うことができる。

app/controllers/users_controller.rb
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

ここではリダイレクト先をプロフィールページからルートURLに変更し、かつユーザーは以前のようにログインしないようになっている。

##アカウントを有効化する
メールが生成できたら、今度はAccountActivationsコントローラのeditアクションを書いていこう。
###authenticated?メソッドの抽象化
ここで、有効化トークンとメールをそれぞれparams[:id]とparams[:email]で参照できることを思い出そう。次のようなコードでユーザーを検索して認証することにする。

user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])

上のコードで使っているauthenticated?メソッドは、アカウント有効化のダイジェストと、渡されたトークンが一致するかどうかをチェックする。ただし、このメソッドは記憶トークン用なので今は正常に動作しない。

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

これを一般化するため、次のようなコードを書く

def authenticated?(attribute, token) #attributeにrememberかactivateが入る
  digest = self.send("#{attribute}_digest") #モデルの属性である〜_digestにアクセス
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token) #渡されたトークンがダイジェストと一致すればtrue
end

上のコードはモデル内にあるので、selfは省略できる。
ここまでできれば、次のように呼び出すことでauthenticated?の従来の振舞いを再現できる。

user.authenticated?(:remember, remember_token)

以上の説明を実際のUserモデルに適用した、抽象化したauthenticated?メソッドをリスト以下に示す。

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

このままだとテストに失敗する。authenticated?が古いままになっており、引数も2つではなくまだ1つのままだから。これを解消するため、両者を更新して、新しい一般的なメソッドを使うようにする。

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

###editアクションで有効化
authenticated?が整備されたことで、やっとeditアクションを書く準備ができた。このアクションは、paramsハッシュで渡されたメールアドレスに対応するユーザーを認証する。ユーザーが有効であることを確認する中核は、次の部分になる。

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

!user.activated?という記述に注意する。このコードは、既に有効になっているユーザーを誤って再度有効化しないために必要である。
上の論理値に基いてユーザーを認証するには、ユーザーを認証してからactivated_atタイムスタンプを更新する必要がある。

user.update_attribute(:activated,    true)
user.update_attribute(:activated_at, Time.zone.now)

以上を踏まえると、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

これで、有効化メールのリンクを踏むと、アカウントが有効化されるようになった。
この時点ではユーザーのログイン方法を変更していないので、ユーザーの有効化にはまだ何の意味もない。ユーザーの有効化が役に立つためには、ユーザーが有効である場合にのみログインできるようにログイン方法を変更する必要がある。
有効でないユーザーがログインすることのないようにする

app/controllers/sessions_controller.rb
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

これで、ユーザー有効化機能のおおまかな部分については実装できた。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?