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