Help us understand the problem. What is going on with this article?

Laravelの実装調査 ~パスワードリセット編~

More than 1 year has passed since last update.

Laravelでスキャッフォルドされるパスワードリセット周りの実装がどうなっているのかを調べてみました。
該当箇所のコードを引用しつつ、コメントを付与する形で解説しています。
なお、パスワードリセット周りをカスタマイズする方法を解説している記事ではないので注意してください。
あくまでLaravel内部でどういう処理が行われているのかを調べた記事になります。

調査に使用したLaravelのバージョンは5.7.22です。

関連記事
Laravelの実装調査 ~会員登録編・署名付きリンクもあるよ~
Laravelの実装調査 ~ログイン編・リメンバーログインもあるよ~

今回はパスワードリセット編という事で、以下2ケースをまとめています。

  • パスワードリセット(メール送信)
  • パスワードリセット(変更処理)

それでは見ていきましょう。

パスワードリセット(メール送信)

パスワードを忘れてログイン出来なくなったユーザ向けに、パスワード変更メールを送信する処理です。

ルーティング

Illuminate\Routing\Router.php 1158行目あたり

Router.php
// パスワードリセットメール送信フォーム表示
$this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
// パスワードリセットメール送信
$this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');

バリデーション

Illuminate\Foundation\Auth\SendsPasswordResetEmails.php 48行目あたり

SendsPasswordResetEmails.php
protected function validateEmail(Request $request)
{
    $this->validate($request, ['email' => 'required|email']);
}

処理概要

Illuminate\Auth\Passwords\PasswordBroker.php 55行目あたり

PasswordBroker.php
    public function sendResetLink(array $credentials)
    {
        // メールアドレスからユーザ情報を取得
        $user = $this->getUser($credentials);

        if (is_null($user)) {
            return static::INVALID_USER;
        }

        // パスワードリセットトークンを生成し
        // パスワードリセットメールを送信する
        $user->sendPasswordResetNotification(
            $this->tokens->create($user)
        );

        return static::RESET_LINK_SENT;
    }

メールの内容

名前付きルートpassword.resetへのリンクを生成しているのがわかる。
その際、トークンをルートパラメータとして付与している。
生成されるリンクの書式はpassword/reset/{token}となっている。

Illuminate\Auth\Notifications\ResetPassword.php 53行目あたり

ResetPassword.php
public function toMail($notifiable)
{
    if (static::$toMailCallback) {
        return call_user_func(static::$toMailCallback, $notifiable, $this->token);
    }

    return (new MailMessage)
        ->subject(Lang::getFromJson('Reset Password Notification'))
        ->line(Lang::getFromJson('You are receiving this email because we received a password reset request for your account.'))
        // リンクを生成している
        ->action(Lang::getFromJson('Reset Password'), url(config('app.url').route('password.reset', $this->token, false)))
        ->line(Lang::getFromJson('If you did not request a password reset, no further action is required.'));
}

パスワードリセットトークンについて

トークン作成処理概要

Illuminate\Auth\Passwords\DatabaseTokenRepository.php 74行目あたり

DatabaseTokenRepository.php
public function create(CanResetPasswordContract $user)
{
    $email = $user->getEmailForPasswordReset();

    // DBにパスワードリセットトークンが存在する場合は削除する
    $this->deleteExisting($user);

    // トークン生成
    $token = $this->createNewToken();

    // DBにトークンを保存する
    $this->getTable()->insert($this->getPayload($email, $token));

    // トークンを呼び出し元に返す。
    // このトークンがメールのリンクに付与される
    return $token;
}

トークンの作り方

ランダムな40文字をAPP_KEYをキーとしてsha256でハッシュ化している。

Illuminate\Auth\Passwords\DatabaseTokenRepository.php 170行目あたり

DatabaseTokenRepository.php
    public function createNewToken()
    {
        // $this->hashKeyはAPP_KEYをbase64_decodeした値
        return hash_hmac('sha256', Str::random(40), $this->hashKey);
    }

トークンを保存するDBのスキーマ

メールアドレスが主キーとなっている。

2014_10_12_100000_create_password_resets_table.php
Schema::create('password_resets', function (Blueprint $table) {
    $table->string('email')->index();
    $table->string('token');
    $table->timestamp('created_at')->nullable();
});

保存される情報

トークンをDBに保存する際、もう一度、ハッシュ化する。
パスワードをDBに保存する際にハッシュ化するのと同じ目的だろうか。
ハッシュ化の方法もパスワードのハッシュ化と同様

Illuminate\Auth\Passwords\DatabaseTokenRepository.php 108行目あたり

DatabaseTokenRepository.php
protected function getPayload($email, $token)
{
    return ['email' => $email, 'token' => $this->hasher->make($token), 'created_at' => new Carbon];
}

パスワードリセット(変更処理)

パスワード変更メールに記載されているリンクからパスワードリセットフォームの表示と、実際にパスワードを変更する処理を行います。

ルーティング

Illuminate\Routing\Router.php 1180行目あたり

Router.php
// パスワードリセットフォーム表示
$this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
// パスワードリセット処理
$this->post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');

バリデーション

ここではバリデーションが2種類ある

  1. Requestクラスによるルールベースのバリデーション
  2. PasswordBrokerクラスによるバリデーション

Requestクラスによるルールベースのバリデーションは以下の通り

Illuminate\Foundation\Auth\ResetsPasswords.php 67行目あたり

ResetsPasswords.php
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed|min:6',

PasswordBrokerクラスによるバリデーションは、主に、以下のチェックを行っている。

  • メールアドレスに対応するユーザ情報が存在する事
  • トークンが一致している事、有効期限が過ぎていない事

Illuminate\Auth\Passwords\PasswordBroker.php 112行目あたり

PasswordBroker.php
protected function validateReset(array $credentials)
{
    // メールアドレスからユーザ情報を取得する
    if (is_null($user = $this->getUser($credentials))) {
        return static::INVALID_USER;
    }

    // ポストされたパスワードのバリデーション
    // デフォルトではバリデーションルールの`required|confirmed|min:6`と同じことをチェックしている
    if (! $this->validateNewPassword($credentials)) {
        return static::INVALID_PASSWORD;
    }

    // ポストされたトークンの有効性チェック(次のコードスニペットを参照)
    if (! $this->tokens->exists($user, $credentials['token'])) {
        return static::INVALID_TOKEN;
    }

    return $user;
}

Illuminate\Auth\Passwords\DatabaseTokenRepository.php 120行目あたり

DatabaseTokenRepository.php
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']);
}

処理概要(パスワードリセットフォーム表示)

パスワードをリセットするためのフォームを表示する。
クエリパラメータからトークンメールアドレスを取得し、フォームに埋め込んで返している。
ここではまだバリデーションやトークンの有効性チェックは行っていない。

Illuminate\Foundation\Auth\ResetsPasswords.php 25行目あたり

ResetsPasswords.php
public function showResetForm(Request $request, $token = null)
{
    return view('auth.passwords.reset')->with(
        ['token' => $token, 'email' => $request->email]
    );
}

処理概要(パスワードリセット処理)

実際にパスワードを変更する処理を行う。
パスワードリセットフォームでパスワードを入力&submitすると呼び出される。

Illuminate\Foundation\Auth\ResetsPasswords.php 38行目あたり

ResetsPasswords.php
public function reset(Request $request)
{
    // `Request`クラスによるルールベースのバリデーション
    $request->validate($this->rules(), $this->validationErrorMessages());

    // パスワード変更を試みる(次のコードスニペットを参照)
    // ユーザ情報の存在チェックやトークンの有効性チェックを行う
    // 問題無ければ第二引数のコールバックが呼び出され、DBのユーザ情報(パスワード)を更新する
    $response = $this->broker()->reset(
        $this->credentials($request), function ($user, $password) {

            // DB上のユーザ情報(パスワード)を更新する
            // リメンバートークンの更新やログイン処理も含む(後述)
            $this->resetPassword($user, $password);
        }
    );

    return $response == Password::PASSWORD_RESET
                ? $this->sendResetResponse($request, $response)
                : $this->sendResetFailedResponse($request, $response);
}

Illuminate\Auth\Passwords\PasswordBroker.php 83行目あたり

PasswordBroker.php
public function reset(array $credentials, Closure $callback)
{
    // ユーザ情報取得&バリデーション(次のコードスニペットを参照)
    $user = $this->validateReset($credentials);

    if (! $user instanceof CanResetPasswordContract) {
        return $user;
    }

    $password = $credentials['password'];

    // コールバック呼び出し
    $callback($user, $password);

    // DB上のパスワードリセットトークンを削除
    $this->tokens->delete($user);

    return static::PASSWORD_RESET;
}

DB上のユーザ情報(パスワード)を更新する処理

Illuminate\Foundation\Auth\ResetsPasswords.php 103行目あたり

ResetsPasswords.php
    protected function resetPassword($user, $password)
    {
        // パスワードプロパティに新しいパスワードを設定
        $user->password = Hash::make($password);

        // リメンバートークンを更新(ログイン状態維持を解除)
        $user->setRememberToken(Str::random(60));

        // ユーザ情報を保存
        $user->save();

        event(new PasswordReset($user));

        // ログインする
        $this->guard()->login($user);
    }

今回登場したクラス達

Illuminate          
├ Auth        
│ ├ Notifications   
│ │ └ ResetPassword
│ └ Passwords   
│   ├ DatabaseTokenRepository
│   └ PasswordBroker
├ Foundation      
│ └ Auth    
│   ├ ResetsPasswords
│   └ SendsPasswordResetEmails
└ Routing     
  └ Router  

おわりに

以上、パスワードリセット周りの実装調査でした。
何かしら新しい発見があったなら幸いです。

t-kuni
主にWEBとか
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした