Tech blog 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');
<?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');

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 / Engineer

1997年生まれ、南伊豆出身。学生時代にC#で画像処理アプリケーションを作ったりしていました。業務では主にLaravelを使用してサーバーサイドのプログラミングをしています。趣味はドライブとシミュレーションゲーム。