ユーザーログインがあるなら、パスワードを忘れることがあるのは当然。忘れたらなら、通常はログイン(たいていは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レコードのパスワードの更新を行います。そして最後に使用したトークンの削除となります。なるほど、削除しないと再度のリセットが可能になるので必要なわけですね。