Produced by Fourier

Laravelで汎用的Multi Authを実装する1

Hirayama Hirayama カレンダーアイコン 2023.01.12

Laravel にはデフォルトで認証機能が用意されており、大抵の場合は Laravel Fortify などのパッケージを使用して実装するのが一番ラクです。

ただ、Fortifyを使用してみると分かるのですが、このパッケージはWebサイトに来るユーザーを全員同じ User モデルとして扱うことを前提としているため、 Multi Auth には対応していません。 また、Fortifyに限らず Laravel ではあまり Multi Auth を推奨しておらず、 Multi Auth を実装した公式パッケージはありません。

そこで、本ブログで Multi Auth の実装方法と、その実装を汎用化する方法についての方法を連載形式で解説したいと思います。 第一回目である本記事では、複数のユーザーを認証するログイン機能を実装します。

実装内容

本記事では UserAdmin の2モデルを使用して実装します。 AdminUser をコピペして作るので、実装にほとんど違いはありません。

アプリケーションのインターフェースですが、APIとWebの両方を実装します。 APIはセッション認証とBearerトークン認証の両方を実装します。Bearerトークン認証の実装には laravel/sanctum を使用します。

認証のURLは、Userは http://localhost/user から始まり、Adminは http://localhost/admin から始まるように作ります。

なお、本記事では Laravel 9 を使用しており、既に初期の環境構築は済ませた前提で解説しております。

実装手順

1. Adminクラスを作る

Multi Auth実装のため、まずはデフォルトの User の他に、認証可能な Authenticatable モデルとして Admin を作成します。

AdminUser のコードをコピペしてファイル名とクラス名だけ書き換えればOKです。

2. Configファイルに設定を追加する

新しく Admin モデルを作成したので、 config/auth.php ファイルに追記します。 また、このときに User モデルの設定もわかりやすいように少し書き換えます。

<?php

return [
    'defaults' => [
        // webからuserに書き換え
        'guard' => 'user',
        'passwords' => 'users',
    ],
    'guards' => [
        // guard名をwebからuserに書き換え
        'user' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        // admin guardを追加
        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ]
    ],
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        // admins providerを追加
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ]
    ],
    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'user_password_resets', // 名前をuser_password_resetsに変更
            'expire' => 60,
            'throttle' => 60,
        ],
        // admins passwordを追加
        'admins' => [
            'provider' => 'admins',
            'table' => 'admin_password_resets',
            'expire' => 60,
            'throttle' => 60,
        ]
    ],
    'password_timeout' => 10800,
];

3. パスワードリセットテーブルを追加&修正

先程のconfigファイルでパスワードリセットテーブルの追加と修正を行ったので、関連する 2014_10_12_100000_create_password_resets_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.
     *
     * @return void
     */
    public function up()
    {
        // user_password_resetsに名前変更
        Schema::create('user_password_resets', function (Blueprint $table) {
            $table->string('email')->index();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });

        // admin_password_resetsを追加
        Schema::create('admin_password_resets', function (Blueprint $table) {
            $table->string('email')->index();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }

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

4. スキャフォールディング

Multi Authの実装は1から自分で作るのは大変なので、 Laravel Breeze というパッケージを利用して、下地となるコードを生成します。

このコマンドを実行することによって、認証に必要な実装コードが生成されます。

composer require laravel/breeze
php artisan breeze:install

5. Routeを設定する

スキャフォールディングによって routes ディレクトリの直下に auth.php ファイルが生成され、そこに認証用のルートが設定されています。 ただ、このルートは使用するguardがデフォルトで固定なため、 Multi Auth にするために修正が必要です。

guestとauthでルートを分ける

まずはguest用ルートとauth用ルートでファイルを分割します。 分割したファイルはわかりやすくするため、 routes/auth ディレクトリを作成し、その下にそれぞれ guest.phpauth.php ファイルを作成します。

<?php

use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use Illuminate\Support\Facades\Route;

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');
guest.php
<?php

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

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');
auth.php

web.php ファイルの修正

次に、先程作成した guest.phpauth.phpweb.php で読み込みます。 読み込む際にguestとauthミドルウェアを設定することで、使用するguardを使い分けることができ、 Multi Auth 認証ができます。

<?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:user')->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 () {
   Route::middleware('guest:user')->group(base_path('routes/auth/guest.php'));
   Route::middleware('auth:user')->group(base_path('routes/auth/auth.php'));
});

Route::prefix('admin')->name('admin.')->group(function () {
    Route::middleware('guest:admin')->group(base_path('routes/auth/guest.php'));
    Route::middleware('auth:admin')->group(base_path('routes/auth/auth.php'));
});

login.blade.php ページの修正

とりあえずログインができるようにページ修正します。

@php
    $middleware = last(app('router')->getCurrentRoute()->middleware());
    $guard = explode(':', $middleware)[1];
@endphp

<x-guest-layout>
    <x-auth-card>
        <x-slot name="logo">
            <a href="/">
                <x-application-logo class="w-20 h-20 fill-current text-gray-500" />
            </a>
        </x-slot>

        <!-- Session Status -->
        <x-auth-session-status class="mb-4" :status="session('status')" />

        <form method="POST" action="{{ route("$guard.login") }}">
            @csrf

            <!-- Email Address -->
            <div>
                <x-input-label for="email" :value="__('Email')" />
                <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
                <x-input-error :messages="$errors->get('email')" class="mt-2" />
            </div>

            <!-- Password -->
            <div class="mt-4">
                <x-input-label for="password" :value="__('Password')" />

                <x-text-input id="password" class="block mt-1 w-full"
                                type="password"
                                name="password"
                                required autocomplete="current-password" />

                <x-input-error :messages="$errors->get('password')" class="mt-2" />
            </div>

            <!-- Remember Me -->
            <div class="block mt-4">
                <label for="remember_me" class="inline-flex items-center">
                    <input id="remember_me" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember">
                    <span class="ml-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
                </label>
            </div>

            <div class="flex items-center justify-end mt-4">
                @if (Route::has('password.request'))
                    <a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}">
                        {{ __('Forgot your password?') }}
                    </a>
                @endif

                <x-primary-button class="ml-3">
                    {{ __('Log in') }}
                </x-primary-button>
            </div>
        </form>
    </x-auth-card>
</x-guest-layout>

6. 確認

最後に php artisan server を実行してHTTPアプリケーションサーバーを立ち上げます。

立ち上げ後に以下のuserとadminのログインページにアクセスできるので、それぞれのページにアクセスし、ログインに成功すれば成功です。

まとめ

本記事で基本的なMulti Authの実装方法について解説しました。
ただ、この実装内容ではまだログイン以外のことができないため、次回以降はそれ以外の機能の実装を解説していきます。

Hirayama

Hirayama slash forward icon Engineer

業務では主にPHPやTypeScriptを使用したバックエンドアプリケーションやデスクトップアプリケーションの開発をしています。趣味は登山。

関連記事