第11章アカウントの有効化
アカウント登録の時にメール認証を用いて行う機能を作成する。
1.ユーザーの初期状態は「有効化されていない」(unactivated)にしておく。
2.ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する。
3.有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
4.ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。
5.ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」(activated)に変更する。
11.1AccountActivationsリソース
AccountActivationsリソースを作るために、まずはAccountActivationsコントローラを生成
$ rails generate controller AccountActivations
を実行
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
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コールバックがあることで色々できるようになるって感じですね。
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
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
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
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)と評価される場合、これらの条件がすべて満たされているため、アプリケーションはユーザーのアカウントを有効化する手続きを進めることができます。このように、ユーザーアカウントのセキュリティを確保しつつ、正しいユーザーによるアカウントのアクティベーションを許可する重要なロジックです。
→つまりこのコードがあることによって有効化リンク踏んでもログインから弾いてくれるってことですね。
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週目の方の参考になればいいかなーと思ってます。
それでは!