Laravel9のBreezeで、マルチログインを実装する

Laravel9のBreezeで、マルチログインを実装する Laravel

Laravel9のBreezeで、マルチログインを実装したいと思います。
通常、管理者機能にはログイン機能を付けたりすると思いますが、ユーザーと管理者で別々のログイン機能を付けたいなど、マルチログインが可能なプログラムを作成します。

Breezeのインストール、ユーザー登録、ログインについては以下をご覧ください。

環境

以下の環境で実装しています。
2023年1月に実装した時点の情報になります。

  • PHP:8.1.14
  • Laravel:9.46.0

モデルの作成

デフォルトの認証では、usersテーブルを利用しますが、本記事では、それに加えてadminsテーブルを作成して、認証情報を管理するようにします。

以下のようにモデルファイルとマイグレーションファイルを作成します。

php artisan make:model Admin -m

モデルの内容は、デフォルト(User.php)と同じにしておきます。

<?php

namespace App\Models;

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

class Admin extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

マイグレーションファイルもデフォルト(usersテーブル)と同じにしておきます。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        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();
        });
    }

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

マイグレーションを行います。

php artisan migrate

ガードの設定

デフォルトのガードに加え、admin用のガードを追加します。
/config/auth.php を修正します。

<?php

return [
    ︙
    (中略)
    ︙
    'guards' => [
        'web' => [
            '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_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
        'admins' => [                     // 追加
            'provider' => 'admins',       // 追加
            'table' => 'password_resets', // 追加
            'expire' => 60,               // 追加
            'throttle' => 60,             // 追加
        ],                                // 追加
    ],
    ︙
    (中略)
    ︙
];

ルーティングの設定

ルーティングを追加します。
デフォルトではroutes/auth.php に記載されているので、/routes/admin.php を作成して、同様に設定します。

各コントローラーのパスが変わるのと、指定するmiddlewareを「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;

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('verify-email', [EmailVerificationPromptController::class, '__invoke'])
                ->name('verification.notice');

    Route::get('verify-email/{id}/{hash}', [VerifyEmailController::class, '__invoke'])
                ->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');
});

作成したadmin.phpをweb.phpから読み込むために、以下を追加します。
プレフィックスに「admin」を付けて、ルーティングするようにしています。

use App\Http\Controllers\ProfileController as ProfileOfAdminController;
︙
(中略)
︙
Route::prefix('admin')->name('admin.')->group(function(){
    Route::get('/dashboard', function () {
        return view('admin.dashboard');
    })->middleware(['auth:admin', 'verified'])->name('dashboard');

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

    require __DIR__.'/admin.php';
});

コントローラーの作成

コントローラーを作成します。
デフォルトの認証機能をコピーして、admin用を作成します。
パスを /app/Http/Controllers/Admin としています。

デフォルトの認証機能である /app/Http/Controllers/Auth をコピーして、/app/Http/Controllers/Admin/Auth とします。

また、ProfileControllerなど、auth配下以外のファイルも同様にコピーして作成します。

ビューの作成

ビューはデフォルトのbladeを利用する形にしています。

ビューもコントローラーと同様に、デフォルトの認証機能からコピーします。
resources/views/auth をコピーして、resources/views/admin/auth とします。

また、ダッシュボードやprofileフォルダなど、auth配下以外のファイルも同様にコピーします。

ユーザー登録

/admin/register へのアクセスで、RegisteredUserController.phpのcreateメソッドが実行されるので、ビューの指定をadmin用に変更します。
※各機能で、ビューの指定を同様に変更します。本記事では主要機能のみ記載します。

public function create(): View
{
    return view('admin.auth.register');
}

ビューは、送信先をadmin用に変更します。
※各機能で、送信先の指定を同様に変更します。本記事では主要機能のみ記載します。

<form method="POST" action="{{ route('admin.register') }}">

また、ログインページへのリンクがあるので、リンク先を変更します。

<a (中略) href="{{ route('admin.login') }}">

続いて、送信時にユーザーの登録を行うRegisteredUserController.phpのstoreメソッドを変更します。
以下の内容をadmin用に変更します。

  • メールアドレスのバリデーションで、ユニークチェックはadminsテーブルに対して行う
  • 対象モデルをAdminに変更
  • 登録後に自動でログインを行う際のガードをadmin用に変更
  • 処理完了後のリダイレクト先をadmin用のダッシュボードに変更
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 = Admin::create([
        'name' => $request->name,
        'email' => $request->email,
        'password' => Hash::make($request->password),
    ]);

    event(new Registered($user));

    Auth::guard('admin')->login($user);

    return redirect(RouteServiceProvider::ADMIN_HOME);
}

リダイレクト先はapp/Providers/RouteServiceProvider.phpに定義されているので、admin用を追加します。

public const ADMIN_HOME = '/admin/dashboard';

ダッシュボード

ユーザー登録が完了すると、ダッシュボードが表示されます。

まず、レイアウトファイルを変更します。
resources/views/layouts/app.blade.php をコピーして、resources/views/layouts/admin.blade.php を作成します。

ナビゲーションメニューがインクルードされているので、admin用に変更します。

@include('layouts.admin_navigation')

ナビゲーションメニューも同様に、resources/views/layouts/navigation.blade.php をコピーして、resources/views/layouts/admin_navigation.blade.php を作成します。

admin用に以下を変更します。(レスポンシブ用も同様に変更)

  • ダッシュボードのリンク先(2箇所、内1つはrouteIsの判定も変更)
  • プロフィールのリンク先
  • ログアウトの送信先とリンク先
︙
(中略)
︙
<div class="shrink-0 flex items-center">
    <a href="{{ route('admin.dashboard') }}">
        <x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
    </a>
</div>

<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
    <x-nav-link :href="route('admin.dashboard')" :active="request()->routeIs('admin.dashboard')">
        {{ __('Dashboard') }}
    </x-nav-link>
</div>
︙
(中略)
︙
<x-dropdown-link :href="route('admin.profile.edit')">
    {{ __('Profile') }}
</x-dropdown-link>

<!-- Authentication -->
<form method="POST" action="{{ route('admin.logout') }}">
    @csrf

    <x-dropdown-link :href="route('admin.logout')"
            onclick="event.preventDefault();
                        this.closest('form').submit();">
        {{ __('Log Out') }}
    </x-dropdown-link>
</form>

dashboard.blade.phpで使用するレイアウトを変更します。

<x-admin-layout>
</x-admin-layout>

また、app/View/Components/AppLayout.php をコピーして、app/View/Components/AdminLayout.php を作成します。

︙
(中略)
︙
class AdminLayout extends Component
{
    public function render(): View
    {
        return view('layouts.admin');
    }
}

また、未ログインでダッシュボードにアクセスした場合は、ログインにリダイレクトするようにします。
app/Http/Middleware/Authenticate.php を変更します。
admin用のリクエスト判定を追加し、adminの場合は、admin用のログインのルーティングを返します。

if (! $request->expectsJson()) {
    if($request->is('admin/*')) return route('admin.login');
    return route('login');
}

ログイン

/admin/login へのアクセスで、AuthenticatedSessionController.phpのcreateメソッドが実行されるので、ビューの指定をadmin用に変更します。

public function create(): View
{
    return view('admin.auth.login');
}

ビューは、送信先をadmin用に変更します。

<form method="POST" action="{{ route('admin.login') }}">

また、パスワードを忘れた場合のリンクがあるので、リンク先を変更します。

@if (Route::has('admin.password.request'))
    <a (中略) href="{{ route('admin.password.request') }}">
        {{ __('Forgot your password?') }}
    </a>
@endif

ログイン時の認証チェックがapp/Http/Requests/Auth/LoginRequest.php で行われます。
admin用のガードを利用してチェックを行うようにします。

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

    $this->is('admin/*') ? $guard = 'admin' : $guard = 'web';

    if (! Auth::guard($guard)->attempt($this->only('email', 'password'), $this->boolean('remember'))) {
        RateLimiter::hit($this->throttleKey());

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

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

ログイン後はダッシュボードにリダイレクトするようにします。

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

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

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

また、ログイン済みでログインにアクセスした場合は、ダッシュボードにリダイレクトするようにします。
app/Http/Middleware/RedirectIfAuthenticated.php を変更します。
ログインチェックをしているところで、admin用のガード判定を追加し、adminの場合は、admin用のダッシュボードにリダイレクトするようにします。

if (Auth::guard($guard)->check()) {
    if($guard == 'admin') return redirect(RouteServiceProvider::ADMIN_HOME); // 追加
    return redirect(RouteServiceProvider::HOME);
}

ログアウト

ログアウト時は、AuthenticatedSessionController.phpのdestoryメソッドが実行されます。
以下をadmin用に変更します。

  • ログアウト時に利用するガードを変更する
  • ログアウト後のリダイレクト先を変更する
public function destroy(Request $request): RedirectResponse
{
    Auth::guard('admin')->logout();

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

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

    return redirect('/admin/login');
}

パスワード再発行

パスワード再発行は、PasswordResetLinkController.phpのcreateメソッドが実行されるので、ビューの指定をadmin用に変更します。

public function create(): View
{
    return view('admin.auth.forgot-password');
}

ビューは、送信先をadmin用に変更します。

<form method="POST" action="{{ route('admin.password.email') }}">

パスワード再発行の処理はstoreメソッドで実行されます。
sendResetLinkメソッドの呼び出しで、ユーザーのチェックを行い、再発行用のメールを送信しますが、そのチェックの際にadmin用の認証チェックを行うようにします。

public function store(Request $request): RedirectResponse
{
    $request->validate([
        'email' => ['required', 'email'],
    ]);

    // We will send the password reset link to this user. Once we have attempted
    // to send the link, we will examine the response then see the message we
    // need to show to the user. Finally, we'll send out a proper response.
    $status = Password::broker('admins')->sendResetLink(
        $request->only('email')
    );

    return $status == Password::RESET_LINK_SENT
                ? back()->with('status', __($status))
                : back()->withInput($request->only('email'))
                        ->withErrors(['email' => __($status)]);
}

続いて、パスワード再発行メールの内容を変更します。
vendor/laravel/framework/src/Illuminate/Auth/Notifications/ResetPassword.php をコピーして、app/Notifications/AdminResetPassword.php を作成します。

buildMailMessageメソッドにて、件名やパスワード有効期限を設定しているので、admin用に変更します。
resetUrlメソッドにて、パスワード再発行を実行するためのURLを生成しているので、変更します。

protected function buildMailMessage($url)
{
    return (new MailMessage)
        ->subject(Lang::get('Admin Reset Password Notification'))
        ->line(Lang::get('You are receiving this email because we received a password reset request for your account.'))
        ->action(Lang::get('Reset Password'), $url)
        ->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.admins.expire')]))
        ->line(Lang::get('If you did not request a password reset, no further action is required.'));
}

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

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

Adminモデルに上記で作成したクラスを実装します。

use App\Notifications\AdminResetPassword as ResetPasswordNotification;
︙
(中略)
︙
public function sendPasswordResetNotification($token)
{
    $this->notify(new ResetPasswordNotification($token));
}

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