Laravel Breezeで新規登録時にメール認証をした後、トップページに遷移させたい
UserテーブルとAdminテーブルがあります。
UserとAdminとではメール認証後のトップページは違うのでそれぞれで分けたいので、メール認証機能をカスタマイズして実装をしました。
マルチ認証の設定
1.Laravel Breezeを導入
マイグレーションファイルとモデルを作成
Userテーブル
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
//email_verified_atがあるか確認
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};
MustVerifyEmail
を追加し、新規ユーザーにメール認証リンクを送信するプロセスを設定します
Adminテーブル
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('admins', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
//email_verified_atがあるか確認
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('admins');
}
};
guardsを作成
'guards' => [
'user' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
/**
* 省略
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
/**
* 省略
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
'admins' => [
'provider' => 'admins',
'table' => 'password_resets',
'expire' => 60,
],
Requests/Auth/LoginRequest.phpの
のauthenticate
メソッドを修正します
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
$guardName = 'user';
if ($this->is('admin/*')) {
$guardName = 'admin';
}
if (! Auth::guard($guardName)->attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip());
}
}
ミドルウェアでルートを定義
メール送信後に元の画面(新規登録画面)を表示させるには、以下のルートはミドルウェアに含めません。
今回はUser/Auth/RegisteredUserController.php
で新規登録時にguardでログイン状態にしているため、ミドルウェアを適応させるとルートがないといったエラーが起きます。
コードは下記にあります。
Route::get('login', [UserAuthenticatedSessionController::class, 'create'])->name('user.login');
Route::get('register', [RegisteredUserController::class, 'create'])
->name('user.register');
UserとAdminのルートを定義します。
<?php
// ユーザー向けのコントローラー
use App\Http\Controllers\User\Auth\AuthenticatedSessionController as UserAuthenticatedSessionController;
use App\Http\Controllers\User\Auth\ConfirmablePasswordController as UserConfirmablePasswordController;
use App\Http\Controllers\User\Auth\EmailVerificationNotificationController as UserEmailVerificationNotificationController;
use App\Http\Controllers\User\Auth\EmailVerificationPromptController as UserEmailVerificationPromptController;
use App\Http\Controllers\User\Auth\NewPasswordController as UserNewPasswordController;
use App\Http\Controllers\User\Auth\PasswordController as UserPasswordController;
use App\Http\Controllers\User\Auth\PasswordResetLinkController as UserPasswordResetLinkController;
use App\Http\Controllers\User\Auth\RegisteredUserController;
use App\Http\Controllers\User\Auth\VerifyEmailController as UserVerifyEmailController;
// 管理者向けのコントローラー
use App\Http\Controllers\Admin\Auth\AuthenticatedSessionController as AdminAuthenticatedSessionController;
use App\Http\Controllers\Admin\Auth\ConfirmablePasswordController as AdminConfirmablePasswordController;
use App\Http\Controllers\Admin\Auth\EmailVerificationNotificationController as AdminEmailVerificationNotificationController;
use App\Http\Controllers\Admin\Auth\EmailVerificationPromptController as AdminEmailVerificationPromptController;
use App\Http\Controllers\Admin\Auth\NewPasswordController as AdminNewPasswordController;
use App\Http\Controllers\Admin\Auth\PasswordController as AdminPasswordController;
use App\Http\Controllers\Admin\Auth\PasswordResetLinkController as AdminPasswordResetLinkController;
use App\Http\Controllers\Admin\Auth\RegisteredUserController as AdminRegisteredUserController;
use App\Http\Controllers\Admin\Auth\VerifyEmailController as AdminVerifyEmailController;
// ユーザー向けの認証ルート
Route::prefix('user')->group(function () {
Route::get('login', [UserAuthenticatedSessionController::class, 'create'])->name('user.login');
Route::get('register', [RegisteredUserController::class, 'create'])
->name('user.register');
// ユーザー向けの認証ルート
Route::middleware('guest:user')->group(function () {
Route::post('register', [RegisteredUserController::class, 'store'])->name('user.register.store');
Route::post('login', [UserAuthenticatedSessionController::class, 'store'])
->name('user.login.store');
Route::get('forgot-password', [UserPasswordResetLinkController::class, 'create'])
->name('user.password.request');
Route::post('forgot-password', [UserPasswordResetLinkController::class, 'store'])
->name('user.password.email');
Route::get('reset-password/{token}', [UserNewPasswordController::class, 'create'])
->name('password.reset');
Route::post('reset-password',[UserNewPasswordController::class, 'store'])
->name('password.store');
});
// 認証済みのユーザールート
Route::middleware('auth:user')->group(function () {
Route::get('verify-email', UserEmailVerificationPromptController::class)
->name('user.verification.notice');
Route::get('verify-email/{id}/{hash}', UserVerifyEmailController::class)
->middleware(['signed', 'throttle:6,1'])
->name('user.verification.verify');
Route::post('email/verification-notification', [UserEmailVerificationNotificationController::class, 'store'])
->middleware('throttle:6,1')
->name('user.verification.send');
Route::get('confirm-password', [UserConfirmablePasswordController::class, 'show'])
->name('user.password.confirm');
Route::post('confirm-password', [UserConfirmablePasswordController::class, 'store']);
Route::put('password', [UserPasswordController::class, 'update'])->name('user.password.update');
Route::post('logout', [UserAuthenticatedSessionController::class, 'destroy'])
->name('user.logout');
});
//メールのリンクを踏んだ後は、以下のルートに遷移させるため用意します
Route::controller(UserController::class)->group(function () {
Route::get('/home', 'home')->name('user.home');
});
});
// 管理者向けの認証ルート
Route::prefix('admin')->name('admin.')->group(function () {
Route::middleware('guest:admin')->group(function () {
Route::get('login', [AdminAuthenticatedSessionController::class, 'create'])->name('login');
Route::post('login', [AdminAuthenticatedSessionController::class, 'store'])->name('login.store');
Route::get('register', [AdminRegisteredUserController::class, 'create'])->name('register');
Route::post('register', [AdminRegisteredUserController::class, 'store'])->name('register.store');
Route::get('forgot-password', [AdminPasswordResetLinkController::class, 'create'])->name('password.request');
Route::post('forgot-password', [AdminPasswordResetLinkController::class, 'store'])->name('password.email');
Route::get('reset-password/{token}', [AdminNewPasswordController::class, 'create'])->name('password.reset');
Route::post('reset-password', [AdminNewPasswordController::class, 'store'])->name('password.update');
});
Route::middleware('auth:admin')->group(function () {
Route::get('verify-email', [AdminEmailVerificationPromptController::class, '__invoke'])->name('verification.notice');
Route::get('verify-email/{id}/{hash}', [AdminVerifyEmailController::class, '__invoke'])->name('verification.verify')->middleware(['signed', 'throttle:6,1']);
Route::post('email/verification-notification', [AdminEmailVerificationNotificationController::class, 'store'])->name('verification.send')->middleware('throttle:6,1');
Route::get('confirm-password', [AdminConfirmablePasswordController::class, 'show'])->name('password.confirm');
Route::post('confirm-password', [AdminConfirmablePasswordController::class, 'store']);
Route::post('logout', [AdminAuthenticatedSessionController::class, 'destroy'])->name('logout');
});
});
コントローラー内にUserとAdminフォルダを作成
Laravel Breezeで作成された、Authフォルダをコピ-して、UserフォルダとAdminフォルダ内に貼り付けます。
3.resources内のviewsフォルダにAdmin、Userフォルダを作成。
Laravel Breezeで作成された、Authフォルダをコピ-して、UserフォルダとAdminフォルダ内に貼り付けます。
Adminの場合
- app
- Http
- Controllers
- User
- Auth
//例
- AuthenticatedSessionController.php
- //以下ファイル
namespace をnamespace App\Http\Controllers\User\Auth
とします。
<?php
namespace App\Http\Controllers\User\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
//各コントローラー
マルチ認証の実装はこれで完了です。
メール認証の実装に入ります。
メール認証
モデルの準備
- Userモデルに
Illuminate\Contracts\Auth\MustVerifyEmail
を実装します。このインターフェースをモデルに実装することで、そのモデル(この場合は Admin モデル)のインスタンスがメールアドレスの確認を必要とすることをLaravelに伝えます。これにより、ユーザーがアカウントを作成した後、メールアドレスを確認するまで、認証が必要な機能やページへのアクセスが制限されます。 -
Notifiable
でモデルインスタンスを通じて様々なタイプの通史を送信できるようにします。
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable implements MustVerifyEmail
{
use Notifiable;
// ...
}
新規登録コントローラー設定
<?php
namespace App\Http\Controllers\User\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): View
{
return view('user.auth.register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:'.Admin::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::guard('user')->login($user);
return redirect()->route('user.register');
}
ここで新規登録アクションを実行すると、verification.verify not defined
となります。
メール確認通知を返すルートの名前は verification.noticeにする必要があります。Laravelが用意しているverifiedミドルウェアは、ユーザーがメールアドレスを確認していない場合、このルート名に自動的にリダイレクトするため、ルートへ正確にこの名前を割り当てることが重要です。
しかし、ルートに名前をつけているのでデフォルトのままでは動作しません。
メール認証をカスタマイズします
Route::get('verify-email/{id}/{hash}', UserVerifyEmailController::class)
->middleware(['signed', 'throttle:6,1'])
->name('user.verification.verify');
ここでVerifyEmailController
の以下のようにします。
<?php
namespace App\Http\Controllers\User\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
class VerifyEmailController extends Controller
{
/**
* メールアドレスを認証する
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
//メール認証されている時のリダイレクト先
if ($request->user()->hasVerifiedEmail()) {
return redirect()->route('user.home');
}
// この行は、ユーザーのメールアドレスが確認されたときに、Verified イベントを発火させるコード
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
//最終的に任意のルート先にリダイレクトさせるようにします
return redirect()->route('user.home');
}
}
カスタムメール認証作成
Laravelのartisanコマンドを使用して、新しい通知クラスを生成します。ターミナルで以下のコマンドを実行してください。
php artisan make:notification UserCustomVerifyEmail
中身は以下のようにします。
<?php
namespace App\Notifications;
use Illuminate\Auth\Notifications\VerifyEmail as VerifyEmailBase;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
class UserCustomVerifyEmail extends VerifyEmailBase
{
/**
* メールアドレスの確認URLを生成
*/
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
//ここに名前付きルートを記述
'user.verification.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
['id' => $notifiable->getKey(), 'hash' => sha1($notifiable->getEmailForVerification())]
);
}
/**
* メールアドレスの確認メールを送信
*/
public function toMail($notifiable)
{
$verificationUrl = $this->verificationUrl($notifiable);
return (new MailMessage)
->subject('メールアドレスの確認')
->line('以下のボタンをクリックしてメールアドレスを確認してください。')
->action('メールアドレスを確認', $verificationUrl);
}
}
解説
VerifyEmailBase
を継承します。「継承」とは、とあるクラスで定義されているものを引き継ぐことです。VerifyEmailBase
は以下のようになっています。
<?php
namespace Illuminate\Auth\Notifications;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\URL;
class VerifyEmail extends Notification
{
/**
* The callback that should be used to create the verify email URL.
*
* @var \Closure|null
*/
public static $createUrlCallback;
/**
* The callback that should be used to build the mail message.
*
* @var \Closure|null
*/
public static $toMailCallback;
/**
* Get the notification's channels.
*
* @param mixed $notifiable
* @return array|string
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$verificationUrl = $this->verificationUrl($notifiable);
if (static::$toMailCallback) {
return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl);
}
return $this->buildMailMessage($verificationUrl);
}
/**
* Get the verify email notification mail message for the given URL.
*
* @param string $url
* @return \Illuminate\Notifications\Messages\MailMessage
*/
protected function buildMailMessage($url)
{
return (new MailMessage)
->subject(Lang::get('Verify Email Address'))
->line(Lang::get('Please click the button below to verify your email address.'))
->action(Lang::get('Verify Email Address'), $url)
->line(Lang::get('If you did not create an account, no further action is required.'));
}
/**
* Get the verification URL for the given notifiable.
*
* @param mixed $notifiable
* @return string
*/
protected function verificationUrl($notifiable)
{
if (static::$createUrlCallback) {
return call_user_func(static::$createUrlCallback, $notifiable);
}
return URL::temporarySignedRoute(
'verification.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
]
);
}
/**
* Set a callback that should be used when creating the email verification URL.
*
* @param \Closure $callback
* @return void
*/
public static function createUrlUsing($callback)
{
static::$createUrlCallback = $callback;
}
/**
* Set a callback that should be used when building the notification mail message.
*
* @param \Closure $callback
* @return void
*/
public static function toMailUsing($callback)
{
static::$toMailCallback = $callback;
}
}
ここでverification.verify
が呼ばれているので「継承」してカスタマイズする必要があります。
protected function verificationUrl($notifiable)
{
if (static::$createUrlCallback) {
return call_user_func(static::$createUrlCallback, $notifiable);
}
return URL::temporarySignedRoute(
'verification.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
]
);
}
オーバーライド
sendEmailVerificationNotification
メソッドは、ユーザーにメールアドレス確認の通知を送信するために使用されます。Laravelでは、MustVerifyEmail インターフェースを実装したモデルは、デフォルトでメールアドレス確認の通知を送信する機能を持っていますが、sendEmailVerificationNotification メソッドをオーバーライドすることで、この通知の送信方法をカスタマイズできます。
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use App\Notifications\UserCustomVerifyEmail; //追加
class User extends Authenticatable implements MustVerifyEmail
{
use Notifiable;
// ...
/**
* メール認証通知の送信
*/
public function sendEmailVerificationNotification()
{
$this->notify(new UserCustomVerifyEmail);
}
}
ここまでのステップ
1.新規登録後、メールを送信。
2.画面は新規登録画面のまま。
3.届いたメールのリンクを踏む
4.user.home
にリダイレクト
正しければ、新規登録時はemail_verified_at
がnullだったのが、メール認証後はリンクをクリックした日付が正しく入力されているのが確認されます。
メール認証していないユーザーは制限したいとき
つまりemail_verified_at
がnullの時はトップページを表示させず、ログインページにリダイレクトさせます。
ミドルウェアの作成
pp/Http/Middleware ディレクトリ内に UserRedirectIfEmailNotVerified.php という名前の新しいミドルウェアクラスファイルが作成されます。
php artisan make:middleware UserRedirectIfEmailNotVerified
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class UserRedirectIfEmailNotVerified
{
/**
* メールアドレスが確認されていない場合は、確認メールを再送信する
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// ユーザーがログインしていて、かつemail_verified_atがnullの場合
if (Auth::check() && Auth::user()->email_verified_at === null) {
// メールアドレスの確認メールを再送信
Auth::user()->sendEmailVerificationNotification();
return redirect()->route('user.login');
}
return $next($request);
}
}
ミドルウェアの登録
app/Http/Kernel.php
のrouteMiddleware
内に定義します。
protected $routeMiddleware = [
'user.email.verified' => \App\Http\Middleware\UserRedirectIfEmailNotVerified::class,
];
ログイン後のリダイレクト先に設定します。今回ではログイン後user.home
というルートに遷移するので以下のように設定します。
Route::controller(UserController::class)->group(function () {
Route::get('/home', 'home')->name('user.home');
})->middleware('user.email.verified');
ここまでのステップ
1.ログインフォームに入力後、ログインボタンを押す
2.user.home
の画面に遷移する前にuser.email.verified
が発火
3.email_verified_at
がnullでないか確認。nullだった場合、認証メールを送信。値が正常の時はuser.home
を表示。
同じようにAdminもカスタムメール認証を作成すれば実装できます。