Laravel11×Breezeでマルチ認証。ユーザーと管理者で分ける例

Laravel11×Breezeでマルチ認証 Breeze
※当サイトはアフィリエイト広告を掲載しています。

Webアプリケーションでは、ユーザー用の管理画面(いわゆるマイページ)と、管理者用の管理画面が別々に存在することも多いです。その場合、ログイン画面やDBテーブルも別になることが一般的でしょうか。

このように、1つのアプリケーションで個別の認証を持つことを、マルチ認証(Multi Auth)と言います。

今回はそんなマルチ認証を仕事で実装することになったのですが、機能はシンプル&低予算ということで、Breezeで実装することにしました。

実はこれまでも何度もやっていますが、その場対応でやっているので、何となくの理解です。加えて多くはLaravel9~10時代でした。

そこで今回はLaravel11×Breezeによる、マルチ認証の方法を書き留めておきたいと思います。

世の中にはすでにマルチ認証の記事はたくさんあると思いますが、私の案件の例ですので、適した設定は人それぞれ違うと思います。あくまで参考程度にしてください。

また、Laravel11をベースとしているので、他のバージョンではちょっと異なる場合があります。そのあたりは適宜読みかえてください。

やりたいこと

あらためて現段階でやることを言語化してみます。

  • ユーザー(users)と管理者(admins)として別々のテーブルを持つ
  • 別々のモデルクラス、User.php, Admin.php
  • デフォルトでは/loginなのを、それぞれ/user/login, /admin/loginとしたい

とてもシンプルです。

手順としては、最初からあるBreezeのログインが/loginなのでそれを/user/loginとするところまで先に完了させます。その後、複製する形で管理者版を作る感じです。

ここまでをひな形として作っておけば、使い回しできそうです。

早速進めましょう。

はじめに

Laravelインストール

Laravelのインストールが出来ている状態からはじめます。

まだな方は以下をご参考ください。

Breezeインストール

Breezeも、以下のコマンドでインストール&初期化済みとします。

composer require laravel/breeze --dev
php artisan breeze:install
npm install

php artisan migrateはまだしていません。

セッションをfileに

今回の案件は複数サーバー構成をとらないので、セッションはfileで管理します。

laravel11はデフォルトでdatabaseになっているので、.envSESSION_DRIVER=fileを足します。

# ~略~

SESSION_DRIVER=file

# ~略~

ついでにDBの初期設定やタイムゾーン、メール設定など各自環境にあわせてやっておきます。

セッション管理にDBは使わないので、database\migrations\0001_01_01_000000_create_users_table.phpsessionsテーブルは作らないでおきます。必要になった時に作れば良いかなと(もちろん今作っても可)。

<?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();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });

        Schema::create('password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });

        // コメントアウト
        // Schema::create('sessions', function (Blueprint $table) {
        //     $table->string('id')->primary();
        //     $table->foreignId('user_id')->nullable()->index();
        //     $table->string('ip_address', 45)->nullable();
        //     $table->text('user_agent')->nullable();
        //     $table->longText('payload');
        //     $table->integer('last_activity')->index();
        // });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
        Schema::dropIfExists('password_reset_tokens');
        // コメントアウト
        // Schema::dropIfExists('sessions');
    }
};

ここでマイグレートします。

php artisan migrate

userディレクトリにファイルを移動

/user/loginなどとしてアクセスできるように、既存のユーザー認証を/userに全て移動します。

そのためにいろいろと設定やファイルを移動・整理します。

なぜ移動するのか?

ユーザーに関するBreezのファイルはそのままで、新規で/adminだけ追加で作ってももちろん良いです。

ただ、個人的には/userにまとめた方がスッキリするように思いました。

この作業が不要な方は行う必要はありませんが、今回は行う前提で進めます。

ビューの移動

ユーザー用のディレクトリ、resources\views\web\userディレクトリを作ります。

作成したディレクトリの中に、以下のBreeze関連のディレクトリ、ファイルを放り込みます(=移動)。

  • resources\views\dashboard.php
  • resources\views\auth
  • resources\views\profile

私はWeb用であるwebディレクトリをかませていますが、これは必須ではありません。

加えてレイアウト関連も移動しましょう。

resources\views\layoutsuserディレクトリを作ります。

作成したディレクトリの中に、以下のファイルを放り込みます。

  • app.blade.php
  • guest.blade.php
  • navigation.blade.php

レイアウトファイルをユーザーと管理者で共有する場合は、やらなくてもOKです。

フォームリクエストの移動

ユーザー用のディレクトリ、app\Http\Requests\Userディレクトリを作ります。

作成したディレクトリの中に、以下のファイルを放り込みます。

  • app\Http\Requests\ProfileUpdateRequest.php
  • app\Http\Requests\Auth

こんな感じのディレクトリ構成になります。

ディレクトリ構成が変わるため、移動したファイルのnamespaceのパスを修正しておきます。

namespace等の修正に関しては、ディレクトリ内の一括置き換えが楽です。VSCodeでは、ファイルツリーから対象のディレクトリを選択後、Shift+Alt+Fをした後に、Chift+Ctrl+Hで実現可能です。 大文字、小文字を区別することをお勧めします。

コントローラーの移動

まず、app\Http\Controllers\Userディレクトリを作ります。

そして作成したそのディレクトリに、既存の以下を放り込みます。

  • app\Http\Controllers\ProfileController.php
  • app\Http\Controllers\Auth

これでBreeze関連のファイルが、Userディレクトリに完全に収まりました。

こちらも移動したファイルのnamespaceも併せて変えておきます。

app\Http\Controllers\User\Auth\内のファイル例ですが、3行目を変えてます。

ついでに、先ほどフォームリクエストも移動しているので一式直しておきましょう(6行目)。

<?php
//  以下のようにUserが付く
namespace App\Http\Controllers\User\Auth;

use App\Http\Controllers\Controller;
// ↓Userディレクトリに移動したためこれも修正
use App\Http\Requests\User\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;

class AuthenticatedSessionController extends Controller
{

app\Http\Controllers\User\ProfileController.phpだけは、さらに以下3行目のようにControlleruseする必要がありますね。

<?php
namespace App\Http\Controllers\User;

use App\Http\Controllers\Controller;
use App\Http\Requests\User\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;

class ProfileController extends Controller
{

ルート

auth.phpweb_user.phpに変更し、web.phpも以下の様に整理しました。ここでコメントアウトした行は、この後web_user.phpに移動予定です。

<?php

// use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

// Route::get('/dashboard', function () {
//     return view('dashboard');
// })->middleware(['auth', 'verified'])->name('dashboard');

// Route::middleware('auth')->group(function () {
//     Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
//     Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
//     Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
// });

Route::prefix('user')->name('user.')->group(function () {
    require __DIR__ . '/web_user.php';
});

上記設定で、web_user.php内の定義は強制的に/userというプリフィクスがつくようになります。

そしてweb_user.phpでは、guestauthミドルウェア等(認証部分)を定義しています。

<?php

// 追加(パスにUserを追加)
use App\Http\Controllers\User\Auth\AuthenticatedSessionController;
use App\Http\Controllers\User\Auth\ConfirmablePasswordController;
use App\Http\Controllers\User\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\User\Auth\EmailVerificationPromptController;
use App\Http\Controllers\User\Auth\NewPasswordController;
use App\Http\Controllers\User\Auth\PasswordController;
use App\Http\Controllers\User\Auth\PasswordResetLinkController;
use App\Http\Controllers\User\Auth\RegisteredUserController;
use App\Http\Controllers\User\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;

// 追加(パスにUserを追加)
use App\Http\Controllers\User\ProfileController;

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

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

    Route::get('login', [AuthenticatedSessionController::class, 'create'])
                ->name('login');

    Route::post('login', [AuthenticatedSessionController::class, 'store']);

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

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

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

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

Route::middleware('auth')->group(function () {
     // プロフィール関連をweb.phpから移動
     Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
     Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
     Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
 
    Route::get('verify-email', EmailVerificationPromptController::class)
                ->name('verification.notice');

    Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
                ->middleware(['signed', 'throttle:6,1'])
                ->name('verification.verify');

    Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
                ->middleware('throttle:6,1')
                ->name('verification.send');

    Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
                ->name('password.confirm');

    Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);

    Route::put('password', [PasswordController::class, 'update'])->name('password.update');

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

    // dashboard関連をweb.phpから移動&修正
    Route::middleware('verified')->group(function () {
        Route::get('/dashboard', function () {
            return view('web.user.dashboard');
        })->name('dashboard');
        // ここからverifiedなルートを追加可能
    });
});

ファイルの分け方は案件や好みによります。今回の方法はユーザーのルートが分けられるメリットがあります。反面、web_user.phpが肥大するので、必要に応じて認証部分のファイルを分けたりするとよいかもしれません。

各種パスの変更

さて、ここまででユーザー関連のディレクトリに、ファイルを移動してきました。切りの良いタイミングですので、このあたりで動作確認をしてみます。

設定やルートをいじったので、キャッシュをクリアしておいた方が良いかもです。

php artisan config:clear
php artisan route:clear

各種パスを合わせる必要がある

人によってURLは変わると思いますが、例えばhttp://localhost/user/loginのようなログイン画面を表示してみます。

結論から言えば、以下の様に100%、Internal Server Errorとなります。

なぜなら、ファイルを移動しただけですので、コントローラ・ビュー(blade)内の各種パスがユーザー用に合っていないからです。

これらを改善していく必要があります。1つずつエラー内容を見ながら、修正していけば完了できるはずです。

……これがBreezeマルチ認証の最も面倒くさい部分なのですよね。

以下、私が行った内容をメモしておきます。

レイアウトファイルの修正

先ほどのエラーは、layout.guestというビューが無いと言っています。

該当指定は、app\View\Componentsの中にある、以下の2ファイルにより指定されています。

  • AppLayout.php
  • GuestLayout.php

上記ファイルを一括置換で以下に変更すれば、エラーが出なくなります。

  • return view('layouts.return view('layouts.user.

Bladeのルート・パス指定を修正

この状態でも、まだまだエラーが出ます。loginというルートは無いですよということです。

これは、ルート名にプリフィクスをつけたため、user.login等としないといけないからです。

resource/viewsの中にはこのような指定が数多くあります。これらも一式変更しなければなりません。

これまでと同じように、resource/views内を対象にした検索置き換えで、以下のように一式置き換えます。

  • layouts.layouts.user.
  • route('route('user.
    ↑注:reset-password.blade.phpの $request->route('token')だけは置き変えない
  • routeIs('outeIs('user.
  • Route::has('Route::has('user.
  • @include('profile@include('web.user.profile

……などなど、置き換えます。これでとりあえずはログイン画面が表示されるようになります。

Lara
Lara

置き換え多すぎ……。

コントローラーから読み込むビューの変更

まだまだやることがあります。

ログイン画面は表示されていますが、ダッシュボードを開こうとするとまだ同じエラーがでます。

次は、コントローラー内で指定するルート指定を変更することでエラーを解消します。

具体的には、app\Http\Controllers\User\ディレクトリ内から読み込むビューファイルに、userプリフィクスを付けます。ここでは2回、一括置換で済みます。

1つ目は、該当ディレクトリ内をview('で検索し、view('web.user.に置き換えます。以下の様に7ファイル存在しました。

※初稿ではこちらが最初の置き換えだったのでスクショがありますが気にしないでください。

viewメソッド以外に、routeメソッドでもビューの指定がありますので置き換えなければなりません。

これが2つ目で、先ほどと同一のディレクトリを対象に、以下を置き換えます。

  • route('route('user.

routeの置き換えにはwebは付かないのでご注意ください。

パスワードリセット

これで一見できたかのように見えますが、まだまだやることはたくさんあります。

パスワードをリセットするページhttp://localhost/user/forgot-passwordからリセットをしようとするとエラーが生じています。

パスワードリセットの処理は、以下のファイルに書かれています。

vendor\laravel\framework\src\Illuminate\Auth\Notifications\ResetPassword.php

    protected function resetUrl($notifiable)
    {
        if (static::$createUrlCallback) {
            return call_user_func(static::$createUrlCallback, $notifiable, $this->token);
        }

        return url(route('password.reset', [
            'token' => $this->token,
            'email' => $notifiable->getEmailForPasswordReset(),
        ], false));
    }

フレームワーク内でpassword.resetが指定されてしまうので、問題が生じているわけですね。

このファイルを修正するのはもちろんよろしくありません。ですのでこれを継承したクラスを作成&利用します。

以下のコマンドを打ち込みます。

php artisan make:notification CustomResetPassword

ファイルが出来るので、以下の様に修正します。

<?php

namespace App\Notifications;

// use Illuminate\Bus\Queueable;
// use Illuminate\Contracts\Queue\ShouldQueue;
// use Illuminate\Notifications\Messages\MailMessage;
// use Illuminate\Notifications\Notification;

// 追加
use Illuminate\Auth\Notifications\ResetPassword as BaseResetPassword;

class CustomResetPassword extends BaseResetPassword
{
    // use Queueable;

    /**
     * コンストラクタを置く場合は
     * 親のコンストラクタを呼ばないとエラーになるので
     * コメントアウトしておく
     */
    // public function __construct()
    // {
        //
    // }

    /**
     * Get the reset URL for the given notifiable (user or admin).
     *
     * @param  mixed  $notifiable
     * @return string
     */
    protected function resetUrl($notifiable)
    {
        // 管理者の場合は admin のルート、それ以外のユーザーの場合は user のルートを使用
        if (request()->routeIs('admin.*')) {
            return url(route('admin.password.reset', [
                'token' => $this->token,
                'email' => $notifiable->getEmailForPasswordReset(),
            ], false));
        } else {
            return url(route('user.password.reset', [
                'token' => $this->token,
                'email' => $notifiable->getEmailForPasswordReset(),
            ], false));
        }
    }
}

そしてapp/Models/User.phpモデルで、それを使用するようにします。

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

// 追加
use App\Notifications\CustomResetPassword;


class User extends Authenticatable
{
    //(略)

    // パスワードリセット通知のカスタマイズ
    public function sendPasswordResetNotification($token)
    {
        $this->notify(new CustomResetPassword($token));
    }

    //(略)
}

これでパスワードのリセットができるようになりました。

ログインリダイレクト

表面化しづらいですが、まだ問題があります。まだログインされていない状態に、ダッシュボードhttp://localhost/user/dashboardへリクエストするとエラーになります。

このエラーで指定されているルート:loginは、以下で定義されています。

vendor\laravel\framework\src\Illuminate\Foundation\Configuration\ApplicationBuilder.php

<?php

namespace Illuminate\Foundation\Configuration;

use Closure;
use Illuminate\Console\Application as Artisan;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
use Illuminate\Contracts\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Bootstrap\RegisterProviders;
use Illuminate\Foundation\Events\DiagnosingHealth;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as AppEventServiceProvider;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as AppRouteServiceProvider;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Laravel\Folio\Folio;

class ApplicationBuilder
    // (略)
    public function withMiddleware(?callable $callback = null)
    {
        $this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) {
            $middleware = (new Middleware)
                ->redirectGuestsTo(fn () => route('login'));

            if (! is_null($callback)) {
                $callback($middleware);
            }

こちらもフレームワーク内のファイルを直接修正するわけにはいかないので、アプリケーション内で対策します。

これはミドルウェアで対処することになります。

ミドルウェアは、Laravel11からはbootstrap/app.phpを編集します。Adminはまだ作っていませんが、その処理もついでに書いておきましょう。

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

// 追加
use Illuminate\Http\Request; 

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
         // リクエストルートが admin.* なら admin.login に、それ以外なら user.login にリダイレクト
         $middleware->redirectGuestsTo(function (Request $request) {
            return $request->routeIs('admin.*')
                ? route('admin.login') 
                : route('user.login');
        });
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

これで、未認証時にログイン画面にリダイレクトされるようになりました。

ちなみにLaravel10までは、app\Http\Middleware\Authenticate.phpに書けば良いです。

<?php

namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;

class Authenticate extends Middleware
{
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     */
    protected function redirectTo(Request $request): ?string
    {
        if ($request->expectsJson()) {
            return null;
        }

        if ($request->routeIs('admin.*')) {
            return route('admin.login');
        }

        if ($request->routeIs('user.*')) {
            return route('user.login');
        }

        return route('login');
    }
}

メールアドレス認証

Webアプリケーションでは、メールアドレス認証をする、つまりUserモデルがMustVerifyEmailを実装する場合も多いですよね。

以下の様な感じです。

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

use App\Notifications\CustomResetPassword;

class User extends Authenticatable implements MustVerifyEmail
{

その場合、ユーザー登録時に以下の様にエラーになります。

こちらもフレームワーク内にルートverification.verifyの指定があります。

vendor\laravel\framework\src\Illuminate\Auth\Notifications\VerifyEmail.php

これも、自分で通知クラスを作りオーバーライドします。

以下のコマンドで通知クラスを作成します。

php artisan make:notification CustomVerifyEmail

コマンドにより作られたapp\Notifications\CustomVerifyEmail.phpを、以下のように編集します。

<?php

namespace App\Notifications;

use Illuminate\Auth\Notifications\VerifyEmail as BaseVerifyEmail;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;

class CustomVerifyEmail extends BaseVerifyEmail
{
    protected function verificationUrl($notifiable)
    {
        if (static::$createUrlCallback) {
            return call_user_func(static::$createUrlCallback, $notifiable);
        }

        // 管理者かユーザーかでルート名を切り替え
        $routeName = request()->routeIs('admin.*') 
                 ? 'admin.verification.verify' 
                 : 'user.verification.verify';

        return URL::temporarySignedRoute(
            $routeName, // ルート名を動的に設定
            Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
            [
                'id' => $notifiable->getKey(),
                'hash' => sha1($notifiable->getEmailForVerification()),
            ]
        );
    }
}

上記修正したファイルを、app/Models/User.phpで利用します。

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

use App\Notifications\CustomResetPassword;

// 追加
use App\Notifications\CustomVerifyEmail;

class User extends Authenticatable implements MustVerifyEmail
{
    //(略)
   
    // メール認証通知のカスタマイズ
    public function sendEmailVerificationNotification()
    {
        $this->notify(new CustomVerifyEmail());
    }

    //(略)
}

ミドルウェア

verification.verifyのエラーは解消しましたが、メールアドレス認証が済んでいない状態でダッシュボードにアクセスしようとすると、verification.noticeのエラーが発生します。

これは、vendor\laravel\framework\src\Illuminate\Auth\Middleware\EnsureEmailIsVerified.phpにて指定されています。以下のミドルウェアを作ります。

php artisan make:middleware CustomEnsureEmailIsVerified

app\Http\Middleware\CustomEnsureEmailIsVerified.phpが作られるので、以下の様にコードを修正します。

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\URL;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified as BaseEnsureEmailIsVerified;

// 追加
use Illuminate\Contracts\Auth\MustVerifyEmail;

class CustomEnsureEmailIsVerified extends BaseEnsureEmailIsVerified{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     * @param  string|null  $guard
     * @param  string|null  $redirectToRoute
     */
    public function handle($request, Closure $next, $guard = null, $redirectToRoute = null)
    {
        $user = auth()->guard($guard)->user();
        if (! $user || ($user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail())) {
            $route = $redirectToRoute;
            if ($guard === 'admin') {
                $route = $redirectToRoute ?: 'admin.verification.notice';
            } else {
                $route = $redirectToRoute ?: 'user.verification.notice';
            }

            return $request->expectsJson()
                ? abort(403, 'Your email address is not verified.')
                : Redirect::guest(URL::route($route));
        }

        return $next($request);
    }
}

そしてこのミドルウェアをverifiedとして登録することで、デフォルトの設定を上書きします。

bootstrap/app.phpを以下の様に編集します。

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__ . '/../routes/web.php',
        commands: __DIR__ . '/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {

        // リクエストルートが admin.* なら admin.login に、それ以外なら user.login にリダイレクト
        $middleware->redirectGuestsTo(function (Request $request) {

            return $request->routeIs('admin.*')
                ? route('admin.login')
                : route('user.login');
        });
        
        // エイリアスとして追加
        $middleware->alias([       
            'verified' => \App\Http\Middleware\CustomEnsureEmailIsVerified::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

Laravel10以前は、app/Http/Kernel.phpに登録します。

これでメールアドレス認証がただしく動作するようになりました。

ログイン時のリダイレクト

Laravelでは、ログイン済みの場合に特定の画面にリダイレクトするミドルウェアがあります。

例えばログイン後にhttp://localhost/user/loginなどにアクセスすると、通常ダッシュボードに移動するのですが、現在はトップ(http://localhost/)に移動するようです。

これもきちんと設定しておかなければ不親切なサイトになってしまいます。

これはguestとして\Illuminate\Auth\Middleware\RedirectIfAuthenticated.phpが割り当てられているためです。独自ミドルウェアを作り上書きしてあげます。

以下のコマンドでミドルウェアを作成します。

php artisan make:middleware CustomRedirectIfAuthenticated

app\Http\Middleware\CustomRedirectIfAuthenticated.phpができますので、私は以下の様に、defaultRedirectUri()メソッドをオーバーライドして実現してみました。

<?php

namespace App\Http\Middleware;

// 追加
use Illuminate\Auth\Middleware\RedirectIfAuthenticated as BaseRedirectIfAuthenticated;

class CustomRedirectIfAuthenticated extends BaseRedirectIfAuthenticated
{
     /**
     * Get the default URI the user should be redirected to when they are authenticated.
     */
    protected function defaultRedirectUri(): string
    {
        if (request()->routeIs('admin.*')) {
            return route('admin.dashboard');
        }
        return route('user.dashboard');
    }
}

bootstrap/app.phpから登録します。

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__ . '/../routes/web.php',
        commands: __DIR__ . '/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {

        // リクエストルートが admin.* なら admin.login に、それ以外なら user.login にリダイレクト
        $middleware->redirectGuestsTo(function (Request $request) {

            return $request->routeIs('admin.*')
                ? route('admin.login')
                : route('user.login');
        });
        
        $middleware->alias([
            'verified' => \App\Http\Middleware\CustomEnsureEmailIsVerified::class,
            // 追加
            'guest' => \App\Http\Middleware\CustomRedirectIfAuthenticated::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

これで正しくリダイレクトするようになりました。

Lara
Lara

これでひとまずユーザーは完了です!

Adminを作る

ユーザー関連を作りつつ、ミドルウェア等での切り分け処理もついでに書いてきたので、あとは簡単です。

Adminモデルを作る

\App\Models\Userをコピーし、\App\Models\Adminを作っておきます。

adminガードを作る

config\auth.phpを編集して、adminガードを作ります。

    // ~(略)~

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        // 追加
        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],
    ],


    // ~(略)~

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\User::class),
        ],
        // 追加
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ],
    ],

    // ~(略)~

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],
        // 追加
        'admins' => [
            'provider' => 'admins',
            // セキュリティは別として、password_reset_tokensと共用でも動きはする模様
            'table' => 'admin_password_reset_tokens',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

フォームリクエストを複製する

app\Http\Requests\Userディレクトリを複製します。

そして、コントローラーの時と同様にいろいろ置換します。

意図しない置換を防ぐために、大文字小文字を区別するチェックをいれてから実施するのが良いでしょう。

  • UserAdmin
  • user()user('admin')
  • Auth::attemptAuth::guard('admin')->attempt

Controllerを複製する

app\Http\Controllers\Userディレクトリをコピーし、Adminディレクトリを作ります。

名称やuseなどのパスも全てAdminに変更する必要があります。

以下が置き換え参考例です。

  • UserAdmin
  • guard('web')guard('admin')
  • $request->user() $request->user('admin')
  • route('user.route('admin.
  • view('web.user.view('web.admin.
  • Auth::logout()Auth::guard('admin')->logout()
  • Auth::login(Auth::guard('admin')->login(

ここで重要なのですが、以下のファイルがあります。

app\Http\Controllers\Admin\Auth\RegisteredUserController.php

一括置き換えでUserAdminに変更すると、このファイルだけ、ファイル名とクラス名が合わなくなります。このファイル名はUserのままでいいやという方は、手作業で戻してください。

もしくは、Adminにしたいという場合はファイル名をRegisteredAdminController.phpに変更してください。

私はファイル名を変えないで進めます。

ビューコンポーネントを複製

app\View\Componentsにあるファイルをそれぞれ以下の様な名称で複製します。

  • AppLayout.phpAdminAppLayout.php
  • GuestLayout.phpAdminGuestLayout.php

クラス名やviewメソッドのパラメータも管理者用に変えます。

<?php

namespace App\View\Components;

use Illuminate\View\Component;
use Illuminate\View\View;

class AdminAppLayout extends Component
{
    /**
     * Get the view / contents that represents the component.
     */
    public function render(): View
    {
        return view('layouts.admin.app');
    }
}
<?php

namespace App\View\Components;

use Illuminate\View\Component;
use Illuminate\View\View;

class AdminGuestLayout extends Component
{
    /**
     * Get the view / contents that represents the component.
     */
    public function render(): View
    {
        return view('layouts.admin.guest');
    }
}

ビューレイアウトを複製

resources\views\layoutsにあるuserを複製し、adminを作ります。

同様にディレクトリ内を以下で置き換えます。

  • user.admin.

ビューを複製する

resources\views\web\userを複製してadminディレクトリを作ります。

あとは、resources\views\web\adminディレクトリ内を以下で置き換えます。

  • user.admin.
  • x-app-layoutx-admin-app-layout
  • x-guest-layoutx-admin-guest-layout

マイグレーションファイルを複製する

database\migrations\0001_01_01_000000_create_users_table.phpを参考に、管理者用のマイグレーションファイルを作っておきます。

上記ファイルを複製し、0001_01_03_000000_create_admins_table.phpとします。

私はこんな風にしてみました。

<?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();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });

        Schema::create('admin_password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });

        // Schema::create('admin_sessions', function (Blueprint $table) {
        //     $table->string('id')->primary();
        //     $table->foreignId('user_id')->nullable()->index();
        //     $table->string('ip_address', 45)->nullable();
        //     $table->text('user_agent')->nullable();
        //     $table->longText('payload');
        //     $table->integer('last_activity')->index();
        // });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
        Schema::dropIfExists('password_reset_tokens');
        // Schema::dropIfExists('sessions');
    }
};

終わったら、以下のコマンドでマイグレートしておきます。

php artisan migrate

ルートを作る

routes\web_user.phpを複製して、routes\web_admin.phpを作り、ユーザー用になっているのを、各種置き換えます。

  • UserAdmin
  • useradmin
  • guestguest:dmin
  • authauth:admin
  • verifiedverified:admin
<?php

use App\Http\Controllers\Admin\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Admin\Auth\ConfirmablePasswordController;
use App\Http\Controllers\Admin\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\Admin\Auth\EmailVerificationPromptController;
use App\Http\Controllers\Admin\Auth\NewPasswordController;
use App\Http\Controllers\Admin\Auth\PasswordController;
use App\Http\Controllers\Admin\Auth\PasswordResetLinkController;
use App\Http\Controllers\Admin\Auth\RegisteredUserController;
use App\Http\Controllers\Admin\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;

use App\Http\Controllers\Admin\ProfileController;

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

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

    Route::get('login', [AuthenticatedSessionController::class, 'create'])
                ->name('login');

    Route::post('login', [AuthenticatedSessionController::class, 'store']);

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

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

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

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

Route::middleware('auth:admin')->group(function () {
     // プロフィール関連をweb.phpから移動
     Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
     Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
     Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
 
    Route::get('verify-email', EmailVerificationPromptController::class)
                ->name('verification.notice');

    Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
                ->middleware(['signed', 'throttle:6,1'])
                ->name('verification.verify');

    Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
                ->middleware('throttle:6,1')
                ->name('verification.send');

    Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
                ->name('password.confirm');

    Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);

    Route::put('password', [PasswordController::class, 'update'])->name('password.update');

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


    // dashboard関連をweb.phpから移動&修正
    Route::middleware('verified:admin')->group(function () {
        Route::get('/dashboard', function () {
            return view('web.admin.dashboard');
        })->name('dashboard');
        // ここからverifiedなルートを追加可能
    });
});

ここもRegisteredUserControllerUserのままにした場合は置き換え注意です。一括置き換えできなくて面倒なので、RegisteredAdminControllerにファイル名を変更しておく手もありますね。

ここでは/admin/loginとしていますが、実運用ではURLが想定されてしまうので、IP制限などかけないと問題が起こる可能性があります。それができない場合は、もっと分かりづらいURLにするべきでしょう。

いましがた作ったファイルを、routes\web.phpに追加します。

<?php

// use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

// Route::get('/dashboard', function () {
//     return view('dashboard');
// })->middleware(['auth', 'verified'])->name('dashboard');

// Route::middleware('auth')->group(function () {
//     Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
//     Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
//     Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
// });

Route::prefix('user')->name('user.')->group(function () {
    require __DIR__ . '/web_user.php';
});

Route::prefix('admin')->name('admin.')->group(function () {
    require __DIR__ . '/web_admin.php';
});

その他問題等

パスワードのテーブルがusersが使われてしまう問題

app\Http\Controllers\Admin\Auth\PasswordResetLinkController.phpを以下のように置き換えます。

  • Password::sendResetLinkPassword::broker('admins')->sendResetLink
  • Password::reset →  Password::broker('admins')->reset

まとめ

いやー、面倒臭い。記事にするのは大変でした。これを書くのに、4~5回ほど最初からやり直したくらいです。

Laravel側で、やりやすい方法を用意してくれていたらいいんですけれどね。どうやら推奨されていないっぽくて、期待薄です。

間違い等がある可能性や、後から問題が出る可能性もあると思いますので、適宜加筆・修正したいと思います。

今回は私の例でしたが、参考になるところがありましたら幸いです。

Lara
Lara

くれぐれも、置き換えは慎重に。ディレクトリの対象範囲や、文字を誤って置き換えると、ごちゃごちゃになって、大変です(経験談)。

コメント

タイトルとURLをコピーしました