0
0

Railsチュートリアル第11章学習まとめ

Posted at

第11章アカウントの有効化

アカウント登録の時にメール認証を用いて行う機能を作成する。

1.ユーザーの初期状態は「有効化されていない」(unactivated)にしておく。
2.ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する。
3.有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
4.ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。
5.ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」(activated)に変更する。

11.1AccountActivationsリソース

AccountActivationsリソースを作るために、まずはAccountActivationsコントローラを生成
$ rails generate controller AccountActivationsを実行

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

resources :account_activations, only: [:edit]を追加

11.1AccountActivationsリソース

有効化のメールには一意の有効化トークンが必要
パスワードの実装(第6章)や記憶トークンの実装(第9章)と同じように仮想的な属性を使ってハッシュ化した文字列をデータベースに保存する

user.activation_token
仮想属性の有効化トークンにアクセス

user.authenticated?(:activation, token)
次のようなコードでユーザーを認証できるようになるとのこと

if user.activated? ...
activated属性を追加して論理値を取る

$ rails generate migration add_activation_to_users \
> activation_digest:string activated:boolean activated_at:datetime
db/migrate/[timestamp]_add_activation_to_users.rb
class AddActivationToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :activation_digest, :string
    add_column :users, :activated, :boolean, default: false
    add_column :users, :activated_at, :datetime
  end
end

activatedにデフォルトの論理値falseを追加

$ rails db:migrateを実行

private

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

メソッド参照で、こうするとRailsはcreate_activation_digestというメソッドを探索して、ユーザーを作成する前に実行する.
今回before_createコールバックを使う目的は、トークンとそれに対応するダイジェストを割り当てるためとのこと。

# 永続的セッションのためにユーザーをデータベースに記憶する
def remember
  self.remember_token = User.new_token
  update_attribute(:remember_digest, User.digest(remember_token))
  remember_digest
end

update_attributeの使い方
記憶トークンやダイジェストは既にデータベースに存在するユーザーのために作成されるのに対し、before_createコールバックの方はユーザーが作成される前に呼び出される
コールバックがあることで、User.newで新しいユーザーが定義されると、activation_token属性やactivation_digest属性が得られるようになる。
activation_digest属性は既にデータベースのカラムとの関連付けができあがっているので、ユーザーが保存されるときに一緒に保存される。

とりあえずbefore_createコールバックがあることで色々できるようになるって感じですね。

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

コードを有効化
・ここではbefore_actionでdawncase_emailを実行(すべての文字を小文字にする)
・ハッシュ化をする。

$ rails console
>> User.first.create_activation_digest
`method_missing': private method `create_activation_digest' called for #<User id:・・・> (NoMethodError)

privateから下はrails consoleから確認することができる。

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

メール配信機能を追加していく

#User メーラーの作成
$ rails generate mailer UserMailer account_activation password_reset

account_activationメソッドと
password_resetメソッドが生成(12章必要)

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

application mailer

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

生成されたUserメーラー
user.emailにメール送信し、mailにsubjectキーを引数として渡す。

class ApplicationMailer < ActionMailer::Base
  default from: "user@realdomain.com"
  layout "mailer"
end
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

設定したuserにメールを引き渡す

edit_account_activation_url(@user.activation_token, ...)

URLをトークンで認証させる

https://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit

そうすると最後がトークン化される。

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

メールアドレスにも適用化

11.3アカウントを有効化する

メール生成の部分は割愛
今度はAccountActivationsコントローラのeditアクションを書いていく。

11.3.1authenticated?メソッドの抽象化

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

有効化トークンとメールをそれぞれparams[:id]とparams[:email]で
参照できることを思い出す。

アカウント有効化のダイジェストと、渡されたトークンが一致するかどうかをチェック。
※記憶トークンなので今は動作しない。

# トークンがダイジェストと一致したら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

ここでは、上のコードの"remember"の部分だけを何らかの方法で変数として扱う。

self.FOOBAR_digest

こんな感じ

authenticated?メソッドでは、受け取ったパラメータに応じて呼び出すメソッドを切り替える。
これをメタプログラミングという。

sendメソッドにより渡されたオブジェクトに対して動的な動きを得られる。

sendメソッドを利用してauthenticated?メソッドを書き換える
このメソッドは、remember_digest という具体的な属性名を直接使用しており、他の認証要素にはそのままでは適用できません。

def authenticated?(remember_token)
  digest = self.send("remember_digest")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(remember_token)
end

上のコードの各引数を一般化し、文字列の式展開も利用すると、次のようなコードになるとのこと。
引数の一般化: attribute という引数を導入して、メソッドが動的にどの認証属性の digest をチェックするかを決定できるようにします。
動的メソッド呼び出し: send メソッドを使って、#{attribute}_digest の形で具体的な digest メソッドを呼び出します。これにより、email_digest、api_key_digest など、異なる認証情報に対応するメソッドを一つで管理できます。

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

この最終形の authenticated? メソッドは、どのような認証属性にも柔軟に対応可能で、再利用性が高く、メンテナンス性も向上しています。新たな認証要素が加わる場合も、同じメソッドを使って簡単に拡張できるため、効率的な開発が可能になります。

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)

11.3アカウントを有効化する

paramsハッシュで渡されたメールアドレスに対応するユーザーを認証する。

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

chatGPTで調べると下記意味のようだ。
ユーザーオブジェクトの存在: if user という部分で、ユーザーオブジェクトが存在するかどうかをチェックしています。これは、そもそもユーザーオブジェクトがデータベースに存在することを確認するためのものです。
ユーザーがまだアクティベートされていないこと: !user.activated? という部分で、ユーザーがまだアクティベーションされていないことを確認しています。アクティベーションされていない(つまりアカウントが非アクティブ状態である)ことをチェックすることで、不正なアクセスや二重アクティベーションを防いでいます。
アクティベーショントークンが有効であること: user.authenticated?(:activation, params[:id]) という部分で、ユーザーがアクティベーションリンクをクリックした際に提供されるトークンが有効であるかを確認しています。このトークンは、ユーザーが登録時にメールで受け取るアクティベーションリンクに含まれており、このトークンが正しいことを確認することで、そのユーザーが正当にアクティベーションを試みていることを保証します。
このコードが真(true)と評価される場合、これらの条件がすべて満たされているため、アプリケーションはユーザーのアカウントを有効化する手続きを進めることができます。このように、ユーザーアカウントのセキュリティを確保しつつ、正しいユーザーによるアカウントのアクティベーションを許可する重要なロジックです。

→つまりこのコードがあることによって有効化リンク踏んでもログインから弾いてくれるってことですね。

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
user.update_attribute(:activated,    true)
user.update_attribute(:activated_at, Time.zone.now)

ユーザー認証後タイムスタンプで更新する。
トークンが無効だった場合はリダイレクトされる(滅多にはないケース)

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?
        forwarding_url = session[:forwarding_url]
        reset_session
        params[:session][:remember_me] == '1' ? remember(user) : forget(user)
        log_in user
        redirect_to forwarding_url || 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', status: :unprocessable_entity
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url, status: :see_other
  end
end

defcreate以降
ユーザー検索: フォームから送信されたEメールアドレスを使って、Userモデルからユーザーを検索します。ここでemail.downcaseを使用しているのは、大文字小文字を区別せずに検索するためです。
認証処理: 検索されたユーザーが存在し、かつauthenticateメソッドでパスワードが正しいことを確認します。
アクティベーション確認:
アクティブなユーザー: ユーザーがアクティブであれば(user.activated?がtrue)、セッションをリセットし(reset_session)、ユーザーをログインさせ(log_in user)、ログイン後のリダイレクト先を決定し、リダイレクトします。また、remember_meチェックボックスがオンの場合はユーザーを記憶します。
非アクティブなユーザー: ユーザーがアクティブでない場合は、アクティベーションリンクをチェックするように促すメッセージを表示し、ルートURLにリダイレクトします。
認証失敗: Eメールまたはパスワードが無効である場合、エラーメッセージを表示し、ログインフォームを再表示します。

とりあえずこれでユーザーの有効化設定ができました。

11.5.1本章のまとめ(引用)

・アカウント有効化はActive Recordオブジェクトではないが、セッションの場合と同様にリソースでモデル化できる
・Railsは、メール送信で扱うAction Mailerのアクションとビューを生成できる
・Action MailerではテキストメールとHTMLメールの両方を利用できる
・メーラーアクションで定義したインスタンス変数は、他のアクションやビューと同様、メーラーのビューから参照できる
・ユーザーがアカウントを有効化できるよう、生成したトークンを使って一意のURLを作る
・より安全なアカウント有効化のために、ハッシュ化したトークン(ダイジェスト)を使う
・メーラーのテストと統合テストは、どちらもUserメーラーの振舞いを確認するのに有用
・Mailgunを使うと、production環境からメールを送信できる

感想

実はここの章が最もつまづいたところで現在もなぜかMailgunのメールが送信できずにいます。笑
メールログからメールが正常に送信ができているのですが、Mailgunにうまく連携できず
本章をまとめる上でもチェックしましたが原因が特定できず...。

ここの構築のところでcurrent_userエラーを吐いてしまいなかなかTOPページの表示にもできなかったんです。
nilの場合は参照しないというコードを記述し回避することができました。
改めてここの章で大事だと思ったのはセキュリティ概念ですね。
なぜこのメール有効化機能を実装するのか。それは不正なユーザーや登録ができないように
するための手段であり機能である。

目的を忘れずにこれからもやっていきたいです。

割愛している箇所はありますが、実際にはすでに1週しているので、
1週目の方の参考になればいいかなーと思ってます。

それでは!

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