ユーザーログインがあるなら、パスワードを忘れることがあるのは当然。忘れたらなら、通常はログイン(たいていはEメール)を入力して、パスワードのリセットのリンクを受け取り、リンク先の画面で新規のパスワードを設定します。新しいパスワードを作成して送信してくるサイトもあります。しかし、良く利用するサイトなら、やはり自分が覚えられるパスワードを設定したいです。

そう、この機能もLaravelで提供しています。

まずは、この機能に必要なものをリスト。

  • パスワードリセットリンクを送信してもらう画面
  • パスワードリセットリンクを含むメールのテンプレート
  • 送信するパスワードリセットリンクの有効期限を設定し保持するDBテーブル
  • パスワードリセットリンク先の画面で、新規パスワードを設定する画面

以上です。

パスワードリセットリンクを送信してもらう画面

routes.phpの設定から見てみましょう。

Route::get('password/email','Auth\PasswordController@getEmail');
Route::post('password/email', 'Auth\PasswordController@postEmail'); 

PasswordController.phpは、AuthController.phpと同様にlaravelインストール時に app/Http/Controllers/Authに存在します。

中身は以下にあるように、これまたAuthController.phpと同様にトレイトで構成されているのでほぼ空状態。

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;

class PasswordController extends Controller
{
    use ResetsPasswords;

    public function __construct()
    {
        $this->middleware('guest');
    }
}

ミドルウェアguestが使用されているので、すでにログインしているなら使用できません。

テンプレートは、reources/views/auth/password.blade.phpのファイルとします。

<form method="POST" action="{!! url() !!}/password/email">
    {!! csrf_field() !!}

    <div>
        Eメール
        <input type="email" name="email" value="{{ old('email') }}">
    </div>

    <div>
        <button type="submit">パスワードリセットリンクを送信</button>
    </div>
</form>

これで画面の表示までは完了。次は、送信ボタンを押したときの処理。以下の3つ作業が必要です。

  • 入力バリデーション。Eメールのフォーマットのチェックだけでなく、ユーザーとしてそのEメールがDBに存在するかのチェック
  • ユニークなトークンを発行し、EメールとともにDBに保存
  • パスワードリセットの画面のURLとトークンを合わせてリンクとして、ユーザーのEメールに送信

この3つの作業が行われているか、PasswordControllerで使用されるトレイトのResetsPasswordsを見てみましょう。


namespace Illuminate\Foundation\Auth;
use Illuminate\Http\Request;
use Illuminate\Mail\Message;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

trait ResetsPasswords
{
    public function getEmail()
    {
        return view('auth.password');
    }

    public function postEmail(Request $request)
    {
        $this->validate($request, ['email' => 'required|email']);
        $response = Password::sendResetLink($request->only('email'), function (Message $message) {
            $message->subject($this->getEmailSubject());
        });
        switch ($response) {
            case Password::RESET_LINK_SENT:
                return redirect()->back()->with('status', trans($response));
            case Password::INVALID_USER:
                return redirect()->back()->withErrors(['email' => trans($response)]);
        }
    }
  ...

postEmailがあり画面の入力を受け取り処理しています。入力したEメールのバリデーションがあります。しかしここでは、その他の作業は皆、Password::sendRsetLinkに任せています。もうちょっと追及してみましょう。

Password::sendResetLinkは、PasswordのクラスはFacadeで、実際は以下のPasswordBrokerのクラスが使用されています。sendResetLinkの定義がありますね。


namespace Illuminate\Auth\Passwords;
use Closure;
use UnexpectedValueException;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Contracts\Auth\PasswordBroker as PasswordBrokerContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class PasswordBroker implements PasswordBrokerContract
{
    protected $tokens;
    protected $users;
    protected $mailer;
    protected $emailView;
    protected $passwordValidator;

    public function __construct(TokenRepositoryInterface $tokens,
                                UserProvider $users,
                                MailerContract $mailer,
                                $emailView)
    {
        $this->users = $users;
        $this->mailer = $mailer;
        $this->tokens = $tokens;
        $this->emailView = $emailView;
    }

    public function sendResetLink(array $credentials, Closure $callback = null)
    {
        $user = $this->getUser($credentials);
        if (is_null($user)) {
            return PasswordBrokerContract::INVALID_USER;
        }
        $token = $this->tokens->create($user);
        $this->emailResetLink($user, $token, $callback);
        return PasswordBrokerContract::RESET_LINK_SENT;
    }

    public function emailResetLink(CanResetPasswordContract $user, $token, Closure $callback = null)
    {
        $view = $this->emailView;
        return $this->mailer->send($view, compact('token', 'user'), function ($m) use ($user, $token, $callback) {
            $m->to($user->getEmailForPasswordReset());
            if (! is_null($callback)) {
                call_user_func($callback, $m, $user, $token);
            }
        });
    }
...

$this->getUserでDBテーブルusersに入力したEメールのレコードを取得して、$this->tokens->createでトークンを作成しDBに保存し、$this->emailResetLinkでパスワードリセットリンクを含むメールを送信します。マッチするDBレコードが存在しないならエラーコードを返して画面にエラーを表示となります。

ここにおいてメール送信に関していくつか。

まず、メール送信が行われるので、.envにおいてMAIL_DRIVERなどの設定が必要です。
さらに、config/mail.phpにおいて、fromの設定が必要です。

...
   'from' => ['address' => null, 'name' => null],
...

上のnullには、例えば、’support@gmail.com’、’サポート’のような具体的な値に置き換える必要あります。

次に、送信メールのテンプレートをresources/views/emails/password.blade.phpとして作成する必要あります。

以下のリンクをクリックして、パスワードのリセットができます。

{{ url('password/reset/'.$token) }

パスワードリセットリンク先の画面で、新規パスワードを設定する画面

さて、次はこのリンク先のパスワードリセット画面です。

まずは、routes.phpの設定から、

Route::get('password/reset/{token}', 'Auth\PasswordController@getReset');
Route::post('password/reset', 'Auth\PasswordController@postReset');

こちらもまた、PasswordControllerですね。

テンプレートは、reources/views/auth/reset.blade.phpのファイルとします。hiddenのtokenを忘れなく。

<form method="POST" action="{!! url() !!}/password/reset">
    {!! csrf_field() !!}
   <input type="hidden" name="token" value="{{ $token }}">
    <div>
        Eメール
        <input type="email" name="email" value="{{ old('email') }}">
    </div>

    <div>
        パスワード
        <input type="password" name="password">
    </div>

    <div>
        パスワードの確認
        <input type="password" name="password_confirmation">
    </div>

    <div>
        <button type="submit">パスワードをリセット</button>
    </div>
</form>

さて、この画面でボタンをクリックしたときの処理は、

  • 入力バリデーション。Eメールやパスワードの値チェック。EメールがDBに存在するかのチェック
  • DBのパスワードの値を暗号化して更新

でしょうか?見てみましょう。

まずは、先ほど出てきたトレイトのResetPasswords.php

...
    public function getReset($token = null)
    {
        if (is_null($token)) {
            throw new NotFoundHttpException;
        }
        return view('auth.reset')->with('token', $token);
    }
..
    public function postReset(Request $request)
    {
        $this->validate($request, [
            'token' => 'required',
            'email' => 'required|email',
            'password' => 'required|confirmed|min:6',
        ]);
        $credentials = $request->only(
            'email', 'password', 'password_confirmation', 'token'
        );
        $response = Password::reset($credentials, function ($user, $password) {
            $this->resetPassword($user, $password);
        });
        switch ($response) {
            case Password::PASSWORD_RESET:
                return redirect($this->redirectPath())->with('status', trans($response));
            default:
                return redirect()->back()
                            ->withInput($request->only('email'))
                            ->withErrors(['email' => trans($response)]);
        }
    }
...

入力バリデーションには、トークンが必須ですね。そして、Password::resetで処理を行い、その結果$responseが成功なら、指定のページにリダイレクト。エラーなら、同画面でエラー表示。

Password::resetの中身ですね、問題は。

こちらも、先のPasswordResetBroker.phpでコードされています。


...
    public function reset(array $credentials, Closure $callback)
    {
         $user = $this->validateReset($credentials);
        if (! $user instanceof CanResetPasswordContract) {
            return $user;
        }
        $pass = $credentials['password'];

        call_user_func($callback, $user, $pass);
        $this->tokens->delete($credentials['token']);
        return PasswordBrokerContract::PASSWORD_RESET;
    }

    protected function validateReset(array $credentials)
    {
        if (is_null($user = $this->getUser($credentials))) {
            return PasswordBrokerContract::INVALID_USER;
        }
        if (! $this->validateNewPassword($credentials)) {
            return PasswordBrokerContract::INVALID_PASSWORD;
        }
        if (! $this->tokens->exists($user, $credentials['token'])) {
            return PasswordBrokerContract::INVALID_TOKEN;
        }
        return $user;
    }

    public function validateNewPassword(array $credentials)
    {
        list($password, $confirm) = [
            $credentials['password'],
            $credentials['password_confirmation'],
        ];
        if (isset($this->passwordValidator)) {
            return call_user_func(
                $this->passwordValidator, $credentials) && $password === $confirm;
        }
        return $this->validatePasswordWithDefaults($credentials);
    }
...

resetでは、$this->validateResetで、ユーザーレコードの有無。新規パスワードのバリデーション、そしてトークンの存在と有効期限内かのチェックを行い、callbackでDBレコードのパスワードの更新を行います。そして最後に使用したトークンの削除となります。なるほど、削除しないと再度のリセットが可能になるので必要なわけですね。

By khino