LaravelのPasswordBrokerについて調べる
こんにちは。ファガイです。
今回はLaravelの5.3から仕組みとしてはちょっと入っていたPasswordBrokerについてソースレベルで調べてみます。
チェックしたバージョン
確認したのはLaravel 8のPasswordBrokerの実装です。
まあ実際の実装は5系の頃とそこまで変わってません。
そもそもPasswordBrokerって何
毎回のようにLaravelは名前が日本人には理解しにくいです。。。
Brokerは仲介人みたいな意味があるようです。実際のコードを読んでいくとその理解が付きやすいですが、パスワードのリセット処理とかの流れをこのPasswordBrokerを通してやりますよっていう感じです。
PasswordBrokerのやることは以下です。
- パスワードリセットリンク通知の送信の仲介
- パスワードリセット
- パスワードリセットトークンに関してのバリデーション、スロットリング
- トークンの作成の仲介
といったところになっています。
PasswordBrokerの作られ方
PasswordBroker
はPasswordResetServiceProvider
→PasswordManager
→PasswordBroker
といった感じで3つのクラスを通して作られます。
PasswordBroker
はコンストラクタとして2つのパラメータを持ってます。
/**
* Create a new password broker instance.
*
* @param \Illuminate\Auth\Passwords\TokenRepositoryInterface $tokens
* @param \Illuminate\Contracts\Auth\UserProvider $users
* @return void
*/
public function __construct(TokenRepositoryInterface $tokens,
UserProvider $users)
{
$this->users = $users;
$this->tokens = $tokens;
}
UserProvider
はAuthでもよく使われるやつですね。TokenRepositoryInterface
はまあ名前の通りパスワードtokenのリポジトリでしょう。
今のところはDatabaseTokenRepository
がTokenRepositoryInterface
の実装となります。(このTokenRepositoryInterfaceはよくあるリポジトリパターンの実装としては結構参考になると思います)
PasswordBrokerManagerの役割
PasswordBrokerManager
の役割は名前の通り管理です。
brokerを複数管理できるようにし、呼ばれた際に生成されてなかったらconfigを元にインスタンスを生成して返します。
まあよくある実装ですね。
public function broker($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return $this->brokers[$name] ?? ($this->brokers[$name] = $this->resolve($name));
}
Tokenの作成
まずトークンのベースとなる作成を見ます。
以下はPasswordBrokerのメソッドです。
/**
* Create a new password reset token for the given user.
*
* @param \Illuminate\Contracts\Auth\CanResetPassword $user
* @return string
*/
public function createToken(CanResetPasswordContract $user)
{
return $this->tokens->create($user);
}
TokenRepositoryInterfaceのcreateを呼んでます。
実際のDatabaseTokenRepositoryはこの様になっています。
/**
* Create a new token record.
*
* @param \Illuminate\Contracts\Auth\CanResetPassword $user
* @return string
*/
public function create(CanResetPasswordContract $user)
{
// パスワードリセットに利用するメールアドレスを取得
$email = $user->getEmailForPasswordReset();
// tokenが入ってるテーブルに対象ユーザ(email)のデータがあったら消す
$this->deleteExisting($user);
// ランダムなトークンを生成します。
$token = $this->createNewToken();
$this->getTable()->insert($this->getPayload($email, $token));
return $token;
}
さて、インサート時のgetPayload
メソッドを見てみましょう。
/**
* Build the record payload for the table.
*
* @param string $email
* @param string $token
* @return array
*/
protected function getPayload($email, $token)
{
return ['email' => $email, 'token' => $this->hasher->make($token), 'created_at' => new Carbon];
}
tokenをハッシュ化して入れています。セキュリティを意識した実装です。
実際のDBを見れる人間でも悪用が出来ないようにハッシュ化をしてデータを入れています。
パスワードリセット時には元のトークンをリンクに埋め込んで、それをハッシュ化して比較チェックします。
PasswordBroker::sendResetLinkの処理
おそらく一番使うであろうsendResetLinkの処理を見ていきましょう。
/**
* Send a password reset link to a user.
*
* @param array $credentials
* @param \Closure|null $callback
* @return string
*/
public function sendResetLink(array $credentials, Closure $callback = null)
{
// ユーザを取得
$user = $this->getUser($credentials);
if (is_null($user)) {
return static::INVALID_USER;
}
// 最近作られたトークンがあるか(Throttleに引っかかるか)
if ($this->tokens->recentlyCreatedToken($user)) {
return static::RESET_THROTTLED;
}
$token = $this->tokens->create($user);
if ($callback) {
$callback($user, $token);
} else {
// 通知を発火する
$user->sendPasswordResetNotification($token);
}
return static::RESET_LINK_SENT;
}
getUserの処理
重要なのはこの2行です。
// 受け取ったcredentialsからtokenを除く(理由はfindに引っかからない可能性があるため)
$credentials = Arr::except($credentials, ['token']);
// 主にemail情報を元にUser情報を取得する
$user = $this->users->retrieveByCredentials($credentials);
// あとは存在しなかった場合のエラー処理、返り値はuser。
$this->tokens->recentlyCreatedTokenの処理
この処理は最初よくわかりませんでした。
簡単に説明するとthrottlingが設定されている場合(デフォルト60秒)、以前作成したレコードの追加日時+throttling秒
を現在日時
と比較します。
public function recentlyCreatedToken(CanResetPasswordContract $user)
{
$record = (array) $this->getTable()->where(
'email', $user->getEmailForPasswordReset()
)->first();
return $record && $this->tokenRecentlyCreated($record['created_at']);
}
protected function tokenRecentlyCreated($createdAt)
{
if ($this->throttle <= 0) {
return false;
}
return Carbon::parse($createdAt)->addSeconds(
$this->throttle
)->isFuture(); // isFutureは未来かどうか
}
つまりこの処理は設定されているthrottleの秒数以内に呼び出されているかどうかを判断しています。
結果、以下の処理で弾いているわけです。
if ($this->tokens->recentlyCreatedToken($user)) {
return static::RESET_THROTTLED;
}
$user->sendPasswordResetNotificationの処理
sendPasswordResetNotificationは最終的にはメール送信へ渡しているだけです。
Illuminate\Auth\Passwords\CanResetPassword
トレイトを利用している場合、以下の処理が実行されます。
ここに関しては、ユーザークラスの中で独自実装出来ます。
/**
* Send the password reset notification.
*
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPasswordNotification($token));
}
このnotifyメソッドはtraitのメソッドです。
私の場合だとモデル内でNotifiable
トレイトを利用していたため、notify
メソッドはRoutesNotifications
トレイトのメソッドになります。
sendPasswordResetNotification
メソッドもIlluminate\Auth\Passwords\CanResetPassword
トレイトから呼ばれているとすると
CanResetPassword::sendResetNotification
→ Userクラス
→ Notifiableトレイト
→ RoutesNotifications::notify
を呼び出したことになりますね。
public function notify($instance) // ResetPasswordNotificationインスタンスが入ってくる
{
app(Dispatcher::class)->send($this, $instance);
}
ここからは普通のNotificationと一緒です。ResetPasswordNotification
クラスにはtoMail
メソッドが実装されているので、メール送信メソッドとして呼び出されます。
PasswordBroker::sendResetLinkの返却値に関して
ここで返却している値はステータスです。
実際に使われるコードは以下のような感じです。
return $status == Password::RESET_LINK_SENT
? app(SuccessfulPasswordResetLinkRequestResponse::class, ['status' => $status])
: app(FailedPasswordResetLinkRequestResponse::class, ['status' => $status]);
まとめ
このようにしてPasswordBrokerは出来ています。
処理としてはうまく出来ているのがTokenRepositoryだなと感じますね。throttleとか、tokenをhasherを通してDBに入れることとか。
ではでは。
Discussion