🥰

Laravel11.xでマルチログイン実装(Breeze)

2024/07/04に公開

Laravel10でBreezeを使ったマルチログイン実装の記事を過去に書いたのだが、larave11になって実装に大幅な修正が入ったため、新規に備忘録として残します。

Laravelのインストール

過去の記事を参照
Laravel10.x のインストールからBreezeを使ったマルチ認証まで

Laravel11になって、インストール時にSQliteのデータベースに初期データが入るので、ENVファイルにMySQLを使うように書くことと、初期設定をしたら、いったん、migrate:freshしてMYSQLにデータを入れること。
そして、Breezeのインストールと表示確認まで行います。

UserモデルのMustVerifyEmailは使えるようにしておきます。

Models/User.php
use Illuminate\Contracts\Auth\MustVerifyEmail;//コメントアウトを外す
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

//implements MustVerifyEmail を Authenticatable の後ろに足す
class User extends Authenticatable implements MustVerifyEmail
{
/*** 略 ***/
}

Adminデータの作成

Adminのモデルとデータベースのテーブルを作成します。
・Models/User.php をコピペして、Admin.phpにリネーム。
・database/migrations/create_users_table.phpをコピペしてcreate_admins_table.phpにリネーム。

必要なカラムは適時書き換えでお願いします。
login_name・role・statusは、アプリでログイン名・権限・有効化のカラムとして使う予定ですがこの記事では説明は割愛します。

Admin.php
namespace App\Models;

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

class Admin extends Authenticatable
{

    protected $fillable = [
        'name',
        'email',
        'password',
        'login_name',
        'role',
        'status',
    ];

    /*** 略 ***/
}
create_admins_table.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')->nullable();
            $table->string('email');
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->string('login_name')->unique();
            $table->integer('role')->default(1);
            $table->tinyInteger('status')->default(1);
            $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();
        });

    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('admins');
        Schema::dropIfExists('admin_password_reset_tokens');
    }
};

これでAdminのデータと、パスワード変更用のテーブル構成ができたのでMigrateします。

php artisan migrate

AdminのAuth Guardを設定

Authはconfigフォルダのauth.phpを参照します。
この中で新しくAdminの設定をします。
ほぼusersのコピー&書き換えでOKです。

config/auth.php
    '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',
            'table' => 'admin_password_reset_tokens',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

Admin専用のRoute作成

routesフォルダにauth.phpがあるのでこれをコピペしてadmin.phpにリネームします。
use文のAuthの単語の前にAdmin専用フォルダ名を追加します。
Route::middleware('guest:admin')->group(function ())は、admin認証前のとき。
Route::middleware('auth:admin')->group(function ())は、admin認証後のRouteです。
サイトトップURL/adminでアクセスされたときは、adminのloginに飛ばす処理も書いておきます。

routes/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 App\Http\Controllers\Admin\AdminController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return redirect()->route('admin.login');
});

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 () {

    Route::get('dashboard', function () {
        return view('admin.dashboard');
    })->name('dashboard');

    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');
});

Routeプロバイダの処理

ここが変更のあるところ。
Laravel10では、app/Providers/RouteServiceProvider.phpにリクエストの分岐を記載できましたが、Laravelではこのファイルがありません。
bootstrapにあるapp.phpに記載することになりました。
ついでにMiddlewareもフォルダが無くなり、ここに書くことになったので追加です。
未認証でadminが付くURLをリクエストしたら、adminのログインに飛ばすコードを追加します。

MiddlewareのAuthenticate.php は未認証の遷移先。
MiddlewareのRedirectIfAuthenticated は認証後の遷移ページ。

bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\Facades\Route;//追加
use Illuminate\Http\Request;//追加
use Illuminate\Auth\Middleware\RedirectIfAuthenticated;//追加
use Illuminate\Support\Facades\Auth;//追加

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
        then: function () {
            Route::middleware('web')
                ->prefix('admin')->as('admin.')
                ->group(base_path('routes/admin.php'));
        },//then:から追加
    )
    ->withMiddleware(function (Middleware $middleware) {
        //MiddlewareのAuthenticate.phpに書いてたもの
        $middleware->redirectGuestsTo(function (Request $request) {
            $request->is('admin*') ? route('admin.login') : route('login');
        });

        //MiddlewareのRedirectIfAuthenticatedに書いてたもの
        if (Auth::guard('admin')->check()) {
                return route('admin.dashboard');
            }
            return route('dashboard');
        })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

Login Requestの書き換え

Userがログインするときは、HttpのRequestフォルダにあるAuth/LoginRequestを使って処理しています。これをコピペしてAdminLoginRequestにリネームし、ログイン名とパスワードでログインできるように書き換えるのと、guardがadminでないとログインできないようにします。

AdminLoginRequest.php

    public function rules(): array
    {
        return [
            'login_name' => ['required', 'string'],
            'password' => ['required', 'string'],
        ];
    }

    public function authenticate(): void
    {
        $this->ensureIsNotRateLimited();

        //Guardをチェックするのを追加して、emailをlogin_nameに変える
        if (!Auth::guard('admin')->attempt($this->only('login_name', 'password'), $this->boolean('remember'))) {
            RateLimiter::hit($this->throttleKey());

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


        RateLimiter::clear($this->throttleKey());
    }

Admin専用Authファイルの作成

先ほどroutes/admin.phpを作った際、記入したAdmin¥Auth¥ファイルを作成します。
app/Http/ControllersにAdminフォルダを新規に作成します。
そのAdminフォルダの中にapp/Http/Controllers/Authフォルダをコピペします。
app/Http/Controllers/Admin/Authの中のファイルを編集します。

全てのnamespaceをApp\Http\Controllers\Admin\Auth に修正。
App\Models\Userと書かれてる場所はApp\Models\Adminに変更。
User::もAdmin::に変更。
あとは、Viewの先頭にadminを追加します。例)auth.register ⇒ admin.auth.register
guard()の中はwebからadminへ変更。
この辺は、Laravel10の時と同じなので、こちらに詳しく掲載しています。
Laravel10.x のインストールからBreezeを使ったマルチ認証まで

ユーザー登録時と登録する内容が異なるのであれば、RegisteredUserControllerの中のstore Functionの項目も適時修正します。

resourcesにあるviewsの中にadminフォルダを作成し、views/authフォルダをコピペします。
views/admin/authフォルダの中のファイルを編集します。
各ファイルのroute()と書かれているところを見つけて、adminを追加します。

Adminは、ログイン名とパスワードでログインするように変更したため、LoginBladeファイルも該当のInput項目の書き換えをします。
また、ユーザー登録時と登録する内容が異なるのであれば、register.bladeのフォーム内容も加筆修正します。

admin/auth/login.blade.php
<form method="POST" action="{{ route('admin.login') }}">
                            @csrf
                            <!-- Login Name -->
                            <div>
                                <x-input-label for="login_name" :value="__(Login Name')" />
                                <x-text-input id="login_name" class="block mt-1 w-full" type="text" name="login_name" :value="old('login_name')" required
                                    autofocus />
                                <x-input-error :messages="$errors->get('login_name')" class="mt-2" />
                            </div>
//略

resourcesフォルダのviewsの中に、WEB表示用のBladeファイルが入っています。
viewsのlayoutsフォルダの中には、コンテンツを表示するための共通ファイルが入っています。
ログイン時にはapp.blade、未ログインにはguest.blade、そしてヘッダー用のメニューが記載されているnavigation.bladeです。
admin専用Layoutにするために、appとnavigationをコピーして、adminとadmin-navigationにリネームします。

次に、appフォルダのViewフォルダの中のComponentsにAppLayout.phpとGuestLayout.phpがあるので、AppLayout.phpをコピーしてAdminLayout.phpにリネームし以下のように修正します。

AdminLayout.php
namespace App\View\Components;

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

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

Login して表示されるDashboardを作る

Adminがログイン成功して表示されるダッシュボードを作ります。
resources/viewsにあるdashboard.blade.phpをコピーしてadminフォルダに貼り付けます。
<x-app-layout>と記載のある部分は、x-admin-lauoutに修正します。

必要なライブラリを入れる

デバッグバー、画像編集、ページネーションは必須です。

・barryvdh/laravel-debugbar
ページ下でデバッグ表示します。

composer require barryvdh/laravel-debugbar

・ページ送りのデザイン
laravel-pagination

php artisan vendor:publish --tag=laravel-pagination

インストールしたら、app/ProvidersにあるAppServiceProvider.phpに使うデザインを指定します。

AppServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;//追加

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        if (request()->is('admin/*')) {
            config(['session.cookie' => config('session.cookie_admin')]);
        }

        //追加
        Paginator::defaultView('default');
        Paginator::defaultSimpleView('simple-default');
    }
}

・ストレージリンク追加する

php artisan storage:link

・Intervention Image のインストール

composer require intervention/image

主に使う機能については公式に書いてある。2系とは使い方が違うので注意。
Intervention Image

・2要素認証
Google Authenticatorを使った認証方法。実装方法を記事にしてあります。
Laravelに2要素認証を実装しよう

Discussion