【Laravel】 Breezeの認証周りを完全に理解する

Webアプリケーションを作成するにあたって必須と言えるものがユーザ認証です。アカウントを作成し、ログインした状態でしかできない操作を提供します。まずは最小構成でユーザ認証を作ってみましょう。

Laravel Breeze

Laravel Breezeは、Laravelの認証機能を最小限にシンプルに実装したものです。そのままで使用できますが、ここではどのような構成になっているか見て、自力で作れるように理解できると強みになります。

Breezeを導入すると、ルーティングなどの一部のファイルが上書きされます。そのため、新規のプロジェクトでインストールするべきです。

Breeze導入

Laravel Breezeは、composerで導入することができます。

composer require laravel/breeze --dev

導入したらbreeze:installします。

php artisan breeze:install

最後にnpm install && npm run devしてくれと表示されるので、実行して導入は完了です。

様々なファイルが作成されているので、動作を見ていきましょう。

アカウントの作成

ルートの定義を見る

/registerに移動すると、アカウント作成画面が表示されます。この画面は、app/routes/auth.phpにあるRoute::get('register'...によってルートが定義されています。コントローラはRegisteredUserControllerですね。

Route::middleware('guest')->group(function () {
    Route::get('register', [RegisteredUserController::class, 'create'])
                ->name('register');

    Route::post('register', [RegisteredUserController::class, 'store']);
    // ...
});

Route::middleware('guest')->group(...)内にあるので、guestというミドルウェアが使用されているようです。ミドルウェアはapp/Http/Kernel.phpで名前を付けていました。そこを見ると\App\Http\Middleware\RedirectIfAuthenticated::classが使用されていることが分かります。

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,

    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    // ...
];

ミドルウェアの定義を見ると、ログイン済みユーザはhome画面に移動させられるという構造になっています。

public function handle(Request $request, Closure $next, ...$guards)
{
    $guards = empty($guards) ? [null] : $guards;

    foreach ($guards as $guard) {
        if (Auth::guard($guard)->check()) {
            return redirect(RouteServiceProvider::HOME);
        }
    }

    return $next($request);
}

guardsは、使用するguardの種類です。例えばguest:adminのように:の後に指定します。何もなければ、app/config/auth.phpにあるデフォルトのguardが使用されます。

ビューの定義を見る

コードを追うと、ビューはapp/resources/views/auth/register.blade.phpにあることが分かります。このビューではコンポーネントが利用されています。

随所にある__('Name')は、多言語対応のためのものです。

あとは適当にフォームが並んでいるだけの普通のWebページです。

Register時の流れを見る

アカウント登録時は情報を送信するわけですが、<form method="POST" action="{{ route('register') }}"></form>となっているので、routesを確認すると同コントローラのstoreに飛ぶことが分かります。

バリデーションをしたら、そのままUser::createでモデルを作成して保存していることが分かります。createできるのは、モデル内で$fillableに含まれているからですね。

// ユーザをDBに登録
$user = User::create([
    'name' => $request->name,
    'email' => $request->email,
    'password' => Hash::make($request->password),
]);

登録されたユーザは、Usersテーブルに記録されています。

アカウントの登録は、Userモデルを作成後、Registerdを使用してイベントを発火しています。EventServiceProviderを見ると、このイベントでメアド認証用メールを送信しているように見えますが、実際には認証はありません。

event(new Registered($user));
class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        // event関数にRegisterdクラスが入ったらSendEmailVerificationNotificationに送られる
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];
    // ...
}

Breezeでは、メール認証はしない構成になっています。もし認証したい場合、app/Models/User.phpMustVerifyEmailをimplementする必要があります。

use Illuminate\Contracts\Auth\MustVerifyEmail;

// implements MustVerifyEmail をつける
class User extends Authenticatable implements MustVerifyEmail
{
    // ...
}

これで登録後にメールが送信されるようになります。

送信されたメールは、Laravel Sailで構築している場合、ローカルではhttp://localhost:8025/にアクセスすると見れます。

後はログイン処理をして移動しています。

メール認証

上の作業でメール認証を行う場合、EventServiceProviderを見るとメールが送信されていることが分かります。

メールに埋め込まれているアドレスとルーティングを見ると、http://.../verify-email/{id}/{hash}のGETメソッドが対応しているようです。これはVerifyEmailController__invokeに飛んでいるらしいので、内容を見てみましょう。

まずは、ルートを見るとmiddleware(['signed', 'throttle:6,1'])とあります。このsignedという名前のミドルウェアで、URLの{hash}が合っているかどうかをチェックしています。これはもとからあるミドルウェアなので、実装せずともそのまま使えます。

このミドルウェアは、Kernel.phpを見ると'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,とあります。これが{hash}のチェックをしている本体です。

ハッシュのチェックをクリアしてコントローラに入ると、最初はすでに認証済みかどうか見ているようです。認証済みなら無条件で飛びます。

if ($request->user()->hasVerifiedEmail()) {
    return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
}

redirect()->intended($path)は、セッションの以前のURLに遷移するためのものです。

認証がまだなら、markEmailAsVerifiedで認証済みマークを付けています。すでにハッシュのチェックは済んでいるので、users.email_verifies_atに日付を入れているだけのようです。

認証済みマークを付けたらevent(new Verified($request->user()));でイベントを投げていますが、対応するリスナーがないため、何も起こりません。何かしたい場合はリスナーを紐付けましょう。

hasVerifiedEmailmarkEmailAsVerifiedは、app/Models/User.phpから親クラスを追っていくと実装されていることが分かります。

追っていった先のvendor\laravel\framework\src\Illuminate\Foundation\Auth\User.phpを見ると、細かい使い方はともかくとして、どのような機能があるかは何となく分かると思います。

処理が済んだらメインの画面に移ります。

ログイン

URLとルートを照らし合わせると、

Route::middleware('guest')->group(function () {
    //...
    Route::get('login', [AuthenticatedSessionController::class, 'create'])
                ->name('login');
    //...
});

が該当しますね。

ここからF12でたどると、vendor内のファイルが開かれる場合があります。しかし、実際にはapp内のほうが使用されます。

ログインページのcreateメソッドを見るとただページを出しているだけです。

ログイン画面のビューのコードを見ると、nameloginのページにPOSTしていることがわかるので、AuthenticatedSessionController@storeを呼んでいることが分かります。

public function store(LoginRequest $request)
{
    $request->authenticate();

    $request->session()->regenerate();

    return redirect()->intended(RouteServiceProvider::HOME);
}

みたまま、$request->authenticate();がメインのログイン処理ですね。追っていくとAuth::attemptで認証しています。ここで、

throw ValidationException::withMessages([
    'email' => trans('auth.failed'),
]);

で例外を排出していますが、実際にはエラー表示ではなくログイン画面に戻される挙動です。Laravelに組み込まれているValidationExceptionは例外を扱うものですが、排出した際のページ遷移の挙動を設定できるものになっています。例えば、

throw ValidationException::withMessages([
    'email' => trans('auth.failed'),
])->redirectTo('/');

のようにredirectToを設定すると、バリデーション失敗時にhttp://.../に移動します。

これらの処理と同時に、ログイン失敗が多いとスロットリングするためのカウントとチェックも行われていますね。

ユーザ名とパスワードのチェックをするバリデーションを通過したらセッションを作成して終わりです。

ログアウト

ルートの定義は次のものであることが分かります。

Route::middleware('auth')->group(function () {
    // ...

    Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
                ->name('logout');
});

ログインと同じコントローラのdestoryを指定しています。Route::middleware('auth')とあり、Kernel.phpを見ると'auth' => \App\Http\Middleware\Authenticate::class,のミドルウェアが使用されています。認証されていなければログインページに飛ばすだけのミドルウェアです。

後はログアウト処理です。

Auth::guard('web')->logout();

$request->session()->invalidate();

では、そのままログアウトしています。Auth::guard('web')->logout();がログアウトのメインですが、セッションを破棄することでもログインは不可能になります。Auth::guard('web')->logout();の方では、いわゆるremember meの処理やLogoutをイベントで投げるなどをしているようです。

パスワードを忘れた

リセット用メールの送信

ルートは

Route::middleware('guest')->group(function () {
    // ...

    Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
                ->name('password.request');

    Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
                ->name('password.email');

    // ...
});

ですね。最初にアクセスするのは、login.blade.phpを見るとfotgot-passwordのgetメソッドであることが分かります。

パスワードを忘れた画面でEmailを送信すると、NewPasswordController@storeが呼ばれます。適当に入力をバリデートしたら、メインのリセット用メールの送信処理です。

$status = Password::sendResetLink(
    $request->only('email')
);

Password::sendResetLinkはLaravel組み込みの機能です。パスワードリセット用トークンを作成し、メールを送信しています。あとは送信に成功したかを見て終わりです。

リセットのページ

ルートは

Route::middleware('guest')->group(function () {
    // ...

    Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
                ->name('password.reset');

    Route::post('reset-password', [NewPasswordController::class, 'store'])
                ->name('password.update');

    // ...
});

ページの表示時は、特にトークンのチェックは行われていません。/reset-password/aabbcchogeのようにどんなトークンを入れてもページは表示できます。ただ、フォームに入力して送信しても失敗するようにできています。

フォームは表示されているだけです。

値が送信されると、パスワード形式が正しいかなどの適当にバリデーションが行われた後、リセット処理が行われます。

$status = Password::reset(
    // リクエストの内、一部のパラメータだけ取り出し
    $request->only('email', 'password', 'password_confirmation', 'token'),

    // メインのリセット処理
    function ($user) use ($request) {
        $user->forceFill([
            'password' => Hash::make($request->password),
            'remember_token' => Str::random(60),
        ])->save();

        event(new PasswordReset($user));
    }
);

リセット処理Password::resetが使用されています。これはLaravelの組み込み機能ですが、実際にパスワードのハッシュを生成するのはなんと自力で実装が必要なようです。remember_tokenはログイン状態保持のためのものです。

まとめ

Laravel Breezeの認証機能を一通り理解すると、ログイン処理の理解が深まると思います。同時に、ここでは出てこなかったメソッドも、実装を追うことでどのような機能があるかを把握できたりします。

Breezeは最低限の認証関連の機能を実装してあるので、必ず理解して、グループ管理などの複雑な認証機能も作れるようになっていきましょう。

laravel breeze thumb

役に立ったらシェアしよう!