本項のテーマ
Laravel5.6のコードを読んでいきます。
Laravelには標準で認証周りが用意されています。その中にはパスワードリセット(パスワード忘れた人にメールを送って再度パスワード設定させるアレ)も含まれています。で、パスワードリセットの大まかな動きとしては...
- (利用者が)メールアドレスを入力してもらう
- (システムが)トークンを発行
- (システムが)パスワードリセット用のURL(パラメータには2で発行したトークン)を記載したメールを送信
- (利用者が)URLにアクセスしてパスワードを変更する
みたいな感じになってます。
で、このトークンには有効期限があります。デフォルトは1時間です。これはconfig/auth.php
にて設定が可能です。該当箇所を抜粋するとconfig/auth.php
の以下の部分です。
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60, /// ← コレ
]
]
さて、ではこの有効期限のチェックは一体どこで処理されているのでしょうか?というのがこの記事のテーマになります。
Let's コードリーディング!
コードリーディングが苦手な方向けに順に読んでいきたいと思います。とっとと答えを知りたい人下から読んでいけばいいんじゃないかと。
ちなみになぜか翻訳みたいな日本語になっている箇所が多々ありますが、単なる文章の好みです。
ルーティングを確認
パスワードリセットのためのルーティングで定義されているのはApp\Http\Controllers\Auth\ResetPasswordControllerのresetメソッドとshowResetFormメソッドです。showResetFormメソッドはその名の通りviewの表示しかしてないので、resetメソッドの方を追ってみましょう。
ResetPasswordController@reset
実際にResetPasswordControllerを見ればすぐにわかりと思いますが、resetメソッドなんてものはResetPasswordControllerに定義されていません。resetメソッドはIlluminate\Foundation\Auth\ResetsPasswordsトレイトに定義されているのです。ResetPasswordControllerはこのトレイトをuseしているだけでした。
ResetsPasswords@reset
続いてResetsPasswordsトレイトのresetメソッドを見てみましょう。以下のような記述が見つかります。
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request), function ($user, $password) {
$this->resetPassword($user, $password);
}
);
実際にパスワードをリセットしているのはここのようです。更に追いかけてみます。
public function broker()
{
return Password::broker();
}
$this->broker()ではPassword::broker()を返しているだけでした。このPasswordファサードの実体は Illuminate\Auth\Passwords\PasswordBrokerManager
です。これについては公式が参考になるかと思います。
では、PasswordBrokerManagerクラスに進んでみましょう。
PasswordBrokerManagerクラス
public function broker($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return isset($this->brokers[$name])
? $this->brokers[$name]
: $this->brokers[$name] = $this->resolve($name);
}
最終的にはresolveメソッドの返り値が返っているようです。以下、resolveメソッドの抜粋です。
protected function resolve($name)
{
~~~ 略 ~~~
// The password broker uses a token repository to validate tokens and send user
// password e-mails, as well as validating that password reset process as an
// aggregate service of sorts providing a convenient interface for resets.
return new PasswordBroker(
$this->createTokenRepository($config),
$this->app['auth']->createUserProvider($config['provider'] ?? null)
);
}
Illuminate\Auth\Passwords\PasswordBrokerクラスを返していますね。PasswordBrokerクラスのコンストラクタ第一引数は\Illuminate\Auth\Passwords\TokenRepositoryInterfaceです。createTokenRepositoryメソッドではTokenRepositoryInterfaceの実装であるIlluminate\Auth\Passwords\DatabaseTokenRepositoryクラスを返しています。
こちらも確認しておきましょう。
protected function createTokenRepository(array $config)
{
~~~ 略 ~~~
return new DatabaseTokenRepository(
$this->app['db']->connection($connection),
$this->app['hash'],
$config['table'],
$key,
$config['expire']
);
}
ここでconfigに記述してあるexpireがようやく登場しました。では、DatabaseTokenRepositoryを確認してみましょう。
DatabaseTokenRepositoryクラス
DatabaseTokenRepositoryのコンストラクタの引数として渡されたexpireはメンバ変数expiresに格納されます。expiresを使っている箇所を探してみましょう。tokenExpiredメソッドが見つかるはずです。
protected function tokenExpired($createdAt)
{
return Carbon::parse($createdAt)->addSeconds($this->expires)->isPast();
}
Carbonを使って有効期限が過ぎているかどうかのチェックをしていますね。ちなみにCarbonというのは日付の扱いを容易にするライブラリです。詳しくは公式へどうぞ。
一応これでトークンの有効期限のチェックをしている箇所はわかりました。本項の目的は既に達しました。が、もう少しだけ詳しくみていきましょう。
tokenExpiredメソッドを呼び出しているのは、existsメソッドです。
public function exists(CanResetPasswordContract $user, $token)
{
$record = (array) $this->getTable()->where(
'email', $user->getEmailForPasswordReset()
)->first();
return $record &&
! $this->tokenExpired($record['created_at']) &&
$this->hasher->check($token, $record['token']);
}
有効期限のチェックとトークンの有無のチェックを同時に行なっているようです。このexistsメソッドはどこから呼び出されているのでしょう?そうです。先ほど少し出て来たPasswordBrokerクラスです。
PasswordBrokerクラス
DatabaseTokenRepositoryはPasswordBrokerのコンストラクタでメンバ変数tokensに格納されます。つまり、 DatabaseTokenRepository@existsを$this->tokens->exists()という形で呼び出すことになります。これを行なっているのはvalidateResetメソッドです。
protected function validateReset(array $credentials)
{
~~~ 略 ~~~
if (! $this->tokens->exists($user, $credentials['token'])) {
return static::INVALID_TOKEN;
}
return $user;
}
このvalidateResetメソッドを利用しているのがresetメソッドです。
public function reset(array $credentials, Closure $callback)
{
// If the responses from the validate method is not a user instance, we will
// assume that it is a redirect and simply return it from this method and
// the user is properly redirected having an error message on the post.
$user = $this->validateReset($credentials);
~~~ 略 ~~~
}
このresetメソッドが、ResetsPasswordsトレイトのresetメソッドで呼び出されていたものです。念のため再掲してみましょう。
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request), function ($user, $password) {
$this->resetPassword($user, $password);
}
);
これですべてが明らかになりました!たった1つを除いては...。
新たな疑問:有効期限が過ぎたトークンはいつ消える?
トークンを削除する処理自体はすぐに見つけられます。DatabaseTokenRepositoryクラスのdeleteExpiredメソッドです。
public function deleteExpired()
{
$expiredAt = Carbon::now()->subSeconds($this->expires);
$this->getTable()->where('created_at', '<', $expiredAt)->delete();
}
deleteExpiredメソッドでgrepしてみると、Illuminate\Auth\Console\ClearResetsCommandクラスが見つかると思います。
これはartisanコンソールコマンドです。有効期限が過ぎたトークンは能動的にコマンドを実行しなければ削除されないようです。このコマンドは以下のように利用します。
php artisan auth:clear-resets {name?}
php artisan auth:clear-resets users
name?はない場合、config/auth.phpのdefault.passwordsの値が使われるみたいです。
'defaults' => [
'guard' => 'web',
'passwords' => 'users', /// ← コレ
],
指定する場合は、config/auth.phpに定義したbrokerの名前を指定します。
'passwords' => [
'users' => [ /// ← コレ
'provider' => 'users',
'table' => 'password_resets',
'expire' => 1,
]
],
どうでもいいけど、コレ、「passwords」じゃなくて「brokers」の方がわかりやすいじゃないのかなぁ(愚痴)
結び
間違いあったら指摘オナシャス。