Laravel のパスワードリマインダ (password reset) を複数 Guard に対応させる
前提条件
- Laravel 8.x
- Laravelの組み込み認証サービスを使っている
- 複数の Model を使用した、複数の Guard がある
前提条件の詳細
config/auth.php がこんな感じになっていて、
'guards' => [
'student' => [
'driver' => 'session',
'provider' => 'student_guard_provider',
],
'teacher' => [
'driver' => 'session',
'provider' => 'teacher_guard_provider',
],
'admin' => [
'driver' => 'session',
'provider' => 'admin_guard_provider',
],
],
'providers' => [
'student_guard_provider' => [
'driver' => 'student_user_provider',
'model' => App\Models\Student::class,
],
'teacher_guard_provider' => [
'driver' => 'teacher_user_provider',
'model' => App\Models\Teacher::class,
],
'admin_guard_provider' => [
'driver' => 'admin_user_provider',
'model' => App\Models\Admin::class,
],
],
ログイン画面の実装が、こんな感じです(フォーム画面を表示する部分は割愛)。
/**
* ログイン試行
*
* @param SigninRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function signin(SigninRequest $request)
{
$credentials = $request->only(['email', 'password']);
if (Auth::guard('admin')->attempt($credentials)) {
$request->session()->regenerate();
return redirect()->intended(route('admin.home'));
}
return back()->withErrors(['email' => 'メールアドレスかパスワードが間違っています。']);
}
概ね Laravel 8.x 認証 : ユーザーを手動で認証する のサンプルそのままで、
これを各 Guard 向けに用意しています。
Laravel のパスワードリマインダってどうなってたっけ。
Laravel純正パスワードリマインダーの使用方法については Laravel 8.x パスワードリセット : ルート あたりに書かれています。
公式のサンプルコードはクロージャにゴリゴリ書かれていますが、
Controller (と FormRequest) を使った記述に変換するとこんな感じです。
/**
* リセットメール送信実行
*
* @param ReminderRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function reminder(ReminderRequest $request)
{
$status = Password::sendResetLink(
$request->only('email')
);
if ($status === Password::RESET_LINK_SENT) {
return redirect()->route(('admin.password.reminder.sent'));
} else {
return back()->with('message.error', __($status));
}
}
/**
* リセット実行
*
* @param ResetRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function reset(ResetRequest $request)
{
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
$user->forceFill([
'password' => Hash::make($password)
])->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
}
);
if ($status === Password::PASSWORD_RESET) {
return redirect()->route(('admin.signin'))->with('message.success', 'パスワードを再設定しました。新しいパスワードでログインしてください。');
} else {
return back()->with('message.error', __($status));
}
}
おもむろに登場する Password ファサード に全てぶん投げられていて、
手を加える余地がないように見えます。
というわけで Password ファサードの実態を知るべく、
このあたり↓を探索すると、
/vendor/laravel/framework/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php
/vendor/laravel/framework/src/Illuminate/Auth/Passwords/PasswordBroker.php
どうやら、Auth が複数の Guard を持てるように、
Password も、複数の Broker なるものを持てるようです。
なお、「ブローカー」と聞くと、闇でパスワードを売り捌く怖い人達を連想しますが、
元々は、単なる手続きの仲介人的な語感のようです。
実装
モデル
前提として、パスワードリマインダに使う Model は、
CanResetPasswordContract インターフェイスを実装する必要があります。
基本的には CanResetPassword トレイトを使用するだけでOKです。
テーブル
Password::sendResetLink() は、パスワードリセット専用のトークンを生成し、
対象ユーザのメールアドレスとともに、 password_resets テーブルに格納しています。
しかし、純正の password_resets テーブルは、email カラムしかキーが無いので、
複数 Model ・複数テーブルでユーザを管理している(=Guard間でメールアドレスが重複している可能性がある)以上、これでは足りません。
対象の Model の数だけ、パスワードリセット用のテーブルを作ってしまうのが簡単です。
public function up()
{
Schema::create('s_admin_password_resets', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('s_student_password_resets', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('s_teacher_password_resets', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
config/auth.php
以下の箇所で Broker を定義します。
provider には、Guard で使用しているユーザプロバイダー名を、
table には、上記で作成したテーブル名を記述します。
'passwords' => [
'student' => [
'provider' => 'student_guard_provider',
'table' => 's_student_password_resets',
'expire' => 60,
'throttle' => 60,
],
'teacher' => [
'provider' => 'teacher_guard_provider',
'table' => 's_teacher_password_resets',
'expire' => 60,
'throttle' => 60,
],
'admin' => [
'provider' => 'admin_guard_provider',
'table' => 's_admin_password_resets',
'expire' => 60,
'throttle' => 60,
],
],
コントローラ
Password::sendResetLink()
Password::reset()
となっていた部分を
Password::broker('ブローカー名')->sendResetLink()
Password::broker('ブローカー名')->reset()
と書き換えるだけです。
/**
* リセットメール送信実行
*
* @param ReminderRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function reminder(ReminderRequest $request)
{
$status = Password::broker('admin')->sendResetLink(
$request->only('email')
);
if ($status === Password::RESET_LINK_SENT) {
return redirect()->route(('admin.password.reminder.sent'));
} else {
return back()->with('message.error', __($status));
}
}
/**
* リセット実行
*
* @param ResetRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function reset(ResetRequest $request)
{
$status = Password::broker('admin')->reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
$user->forceFill([
'password' => Hash::make($password)
])->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
}
);
if ($status === Password::PASSWORD_RESET) {
return redirect()->route(('admin.signin'))->with('message.success', 'パスワードを再設定しました。新しいパスワードでログインしてください。');
} else {
return back()->with('message.error', __($status));
}
}
Auth::xxx() と Auth::guard('ガード名')->xxx() の関係と、
まるっきり同じですね。
broker() の指定なしで Password ファサードを呼んだ場合は、
こちらも Auth 同様、 config/auth.php の defaults に定義された
Broker が使用されます。
'defaults' => [
'guard' => 'student', //これがデフォルト Guard
'passwords' => 'student', //これがデフォルト Broker
],
パスワードリセットメール
パスワードリセット画面を Guard ごとに作る以上、
必然的に送信されるメールも分ける必要があります。
メールを送信する処理は、CanResetPassword トレイトの sendPasswordResetNotification() メソッドがやってくれているので、
下記に記載されている通り、各モデルでこれをオーバーライドすれば OK です。
Laravel 8.x パスワードリセット : リセットメールカスタマイズ
オーバーライド元では Notification が使われていますが、
Mailable を使ってメール送信しても何ら問題はないです。
まとめ
わかってしまえば何ということはありませんでした。
PasswordBrokerManager.php と PasswordBroker.php のソースは一見難解でしたが、
AuthManager と Guard の関係と同じだと気付いたら、
あとはほとんど雰囲気で理解できました。
デザインパターンの強みが活きていますね。
Discussion