OHTA412

Laravel9でLaravel Breezeを使ってマルチログイン機能を実装する方法

現在最新のLaravel9でマルチログイン機能を実装する方法です。ユーザーと管理者の2つのアカウント権限を想定しています。

認証ライブラリ

Laravelの認証ライブラリは、Laravel6以降で「Laravel/ui」が使われていました。しかし、Laravel8以降では「Laravel Breeze」と「Jetstream」が登場して、それらが使われるようになりました。

「Jetstream」は高機能な認証ライブラリのため学習コストが高いので、今回は「Laravel Breeze」でマルチログイン機能を実装します。

「Laravel Breeze」のインストール

下記コマンドでLaravel本体をインストールします。「example-app」の部分はプロジェクト名に応じて適宜変更してください。

$ composer create-project laravel/laravel example-app

インストールが終わったら「.env」ファイルにデータベース情報を記載します。

続いて、プロジェクトディレクトリに移動して下記コマンドで「Laravel Breeze」をインストールします。

$ composer require laravel/breeze --dev
$ php artisan breeze:install
$ php artisan migrate
$ npm install
$ npm run dev
ビルドツールがVite(ヴィート)に変更
Laravelのビルドツールは、以前までLaravel Mixという名前でwebpackが使用されていました。しかし、2022年6月からViteに変更されました。
webpackはビルド時に全ての依存関係を解消してからバンドルするので、プロジェクトが大きくなるにつれてビルドに時間がかかっていました。しかし、Viteは変更があったところだけの依存関係を解消するので、高速でビルドすることが可能になりました。
上記コマンドのnpm run devでViteが実行されて、プロジェクトの変更を感知してくれます。

モデルとマイグレーションを作成する

「Laravel Breeze」のインストールが終わったら、ユーザーとしてのログイン機能が実装されています。それに加えて管理者としてのログイン機能を実装していきます。

まずは、下記コマンドで管理者用のモデルとマイグレーションを作成します。末尾に「-m」を付けることでモデルを作りつつ、マイグレーションも作成しています。

$ php artisan make:model Admin -m

モデルの中身をUser.phpからコピーする

認証機能を使用するには、Userクラスを継承する必要があります。User.phpから必要なコードをコピーします。

// app/Models/Admin.php

…略…

use Illuminate\Foundation\Auth\User as Authenticatable; // ここを追加

class Admin extends Authenticatable // ここを変更(継承元を変更)
{
    use HasFactory;

    // ここから追加
    /**
     * 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テーブルからコピーする

管理者用のadminsテーブルのカラムをusersテーブルと同じにします。

コピー元:20xx_xx_xx_xxxxxx_create_users_table
コピー先:20xx_xx_xx_xxxxxx_create_admins_table

// database/migrations/20xx_xx_xx_xxxxxx_create_admins_table.php

…略…

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

…略…

パスワードリセット機能を使用する場合は「password_resets」テーブルも必要なので、下記コマンドでマイグレーションを作成して、内容をコピーします。

$ php artisan make:migration create_admin_password_resets

コピー元:20xx_xx_xx_xxxxxx_create_password_resets
コピー先:20xx_xx_xx_xxxxxx_create_admin_password_resets

// database/migrations/20xx_xx_xx_xxxxxx_create_admin_password_resets.php

…略…

public function up()
{
    Schema::create('admin_password_resets', function (Blueprint $table) {
        // ここから削除
        $table->id();
        $table->timestamps();
        // ここまで削除
        // ここから追加
        $table->string('email')->index();
        $table->string('token');
        $table->timestamp('created_at')->nullable();
        // ここまで追加
    });
}

…略…

ルートを設定する

routesフォルダ内にadmin.phpを作って、そこで管理者用のルートを定義します。必要なコードはweb.phpからコピーします。

コントローラーの読み込み部分(ファイル上部のuse文)は、パスを変更してAdminフォルダを追加しています。現在Controllers直下にAuthフォルダがあるのですが、ユーザー用と管理者用でコントローラーを分けたいので、Adminフォルダを作ってその中にコントローラーをコピーします。

次の手順で設定するのですが、middlewareにguardも追加します。「user」と「admin」という2つのguardを追加することで、どの権限でログインしたかを制御することができます。

middleware('auth')
↓
middleware('auth:admin')

resources/views内に「user」と「admin」の2つのフォルダを作り、authフォルダとdashboard.blade.phpとwelcome.blade.phpをそれぞれに移動させます。それに伴い、viewのパスを変更します。

view('dashboard')
↓
view('admin.dashboard') // resources/views/admin/dashboard.blade.phpファイルの意味
// 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 Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\ProfileController;

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

Route::middleware('auth:admin')->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::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: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フォルダにコピーしましたが、ユーザー用のコントローラーも同じようにUserフォルダを作ってその中に移動させます。

そして、web.phpでrequireされているauth.phpを編集します。

// routes/auth.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;

…略…

Route::middleware('auth:user')->group(function () { // middlewareにuserガードを追加

…略…

routes/web.phpファイル内でProfileControllerの読み込みがされているので、これもUserフォルダを追記します。

// routes\web.php

…略…

use App\Http\Controllers\User\ProfileController; // ここを変更

…略…

ルートサービスプロバイダーの設定を変更する

ルートサービスプロバイダーで下記の2つの情報を設定します。

  • 管理者用ホームルートのパス(ログインしたときのリダイレクト先):下記コード6行目
  • 先ほど定義したルートファイル全体への設定:下記コード17~31行目
// app\Providers\RouteServiceProvider.php

…略…

    public const HOME = '/dashboard';
    public const ADMIN_HOME = '/admin/dashboard'; // 追加

    public function boot()
    {
        $this->configureRateLimiting();

        $this->routes(function () {
            Route::middleware('api')
                ->prefix('api')
                ->group(base_path('routes/api.php'));

            // ここから削除
            Route::middleware('web')
                ->group(base_path('routes/web.php'));
            // ここまで削除
            // ここから追加
            Route::prefix('/')
                ->as('user.')
                ->middleware('web')
                ->group(base_path('routes/web.php'));

            Route::prefix('admin')
                ->as('admin.')
                ->middleware('web')
                ->group(base_path('routes/admin.php'));
            // ここまで追加
        });
    }

…略…

22行目や27行目でルートファサードにprefixを付けることで、グループ内の各ルートに特定のプレフィックスを追加します。

25行目や30行目でbase_pathが使われていますが、これはそのファイル内の全てのルートに対して設定するという意味になります。

ガードとプロバイダを設定する

ガードとプロバイダを、config/auth.phpで設定します。
パスワードリセットを行うときの設定もこのファイルで行います。ユーザー用はすでに用意されいているので、管理者用を追記します。

ガード(guard)とは?
ガードは認証する方法のことで、今回はセッションとクッキーを使う「セッションガード」を使用します。ユーザー(user)と管理者(admin)でそれぞれ設定します。
プロバイダ(provider)とは?
プロバイダは認証するユーザー情報を取得する方法のことで、今回はEloquentモデルを使用します。こちらも、ユーザーと管理者でそれぞれ設定します。
// config/auth.php

…略…

'defaults' => [
    'guard' => 'users', // ここを変更
    'passwords' => 'users',
],

…略…

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    // ここから追加
    'users' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    'admins' => [
        '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' => 'admin_password_resets',
        'expire' => 60,
        'throttle' => 60,
    ],
    // ここまで追加
],

ミドルウェアを設定する

ミドルウェアで下記の2種類を設定します。

  • ログインしていない状態で認証が必要なページにアクセスしたら、ログイン画面にリダイレクトさせる
  • ログインしている状態でログイン画面にアクセスしたら、ホーム画面にリダイレクトさせる

ログインしていないユーザーをリダイレクトする

app/Http/Middleware/Authenticate.phpのファイル内で、ログインしていないユーザーのリダイレクト先を指定します。

ユーザー・管理者どちらのログイン画面にリダイレクトさせるかを、アクセスしたURLによって条件分岐します。

// app/Http/Middleware/Authenticate.php

…略…

use Illuminate\Support\Facades\Route; // ここを追加

…略…

class Authenticate extends Middleware
{
    // ここから追加
    protected $user_route = 'user.login';
    protected $admin_route = 'admin.login';
    // ここまで追加

    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string|null
     */
    protected function redirectTo($request)
    {
        if (! $request->expectsJson()) {
            return route('login'); // ここを削除
            // ここから追加
            if (Route::is('admin.*')) {
                return route($this->admin_route);
            } else {
                return route($this->user_route);
            }
            // ここまで追加
        }
    }
}

24行目の$request->expectsJson()は、ユーザーがログインしている場合にtrueになります。つまり、ログインしていないユーザーの場合、if文の中のルートにリダイレクトされます。

ログインしているユーザーをリダイレクトする

ログインしているユーザーに関しては、app/Http/Middleware/RedirectIfAuthenticated.phpファイル内で設定します。

// app/Http/Middleware/RedirectIfAuthenticated.php

…略…

public function handle(Request $request, Closure $next, ...$guards)
{
    // ここから削除
    $guards = empty($guards) ? [null] : $guards;

    foreach ($guards as $guard) {
        if (Auth::guard($guard)->check()) {
            return redirect(RouteServiceProvider::HOME);
        }
    }
    // ここまで削除
    // ここから追加
    if (Auth::guard('users')->check() && $request->routeIs('user.*')) {
        return redirect(RouteServiceProvider::HOME);
    }
    if (Auth::guard('admins')->check() && $request->routeIs('admin.*')) {
        return redirect(RouteServiceProvider::ADMIN_HOME);
    }
    // ここまで追加

    return $next($request);
}

…略…

17行目と20行目に書いてあるAuth::guard()の引数は、config/auth.phpファイルで設定したガードが入ります。checkメソッドを実行することで、usersあるいはadminsのガードでログインしているかを判定できます。

18行目と21行目に書いてあるリダイレクト先のRouteServiceProvider::ADMIN_HOMEは、ルートサービスプロバイダーの設定時に定義した定数のことです。

リクエストクラスを設定する

app/Http/Request/Auth/LoginRequest.phpファイル内でリクエストクラスの設定をします。

ログインフォームに入力された値からパスワードを比較し認証するのですが、ここにルートに応じたガード処理を追加します。

// app/Http/Request/Auth/LoginRequest.php

…略…

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

    // ここから追加
    if ($this->routeIs('admin.*')) {
        $guard = 'admins';
    } else {
        $guard = 'users';
    }
    // ここまで追加

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

…略…

10~14行目でガードの種類を定義しています。

リクエストクラスなので、routeIsメソッドが使えます。admin関連のルートの場合はadminsガード、そうでない場合はusersガードを使用します。このガード名は、config/auth.phpファイルで設定したガードが入ります。

17行目のAuth::attempt()で認証を試みていますが、ここに下記コードのようにガード処理を追加します。

Auth::attempt( …
↓
Auth::guard($guard)->attempt( …

コントローラーを設定する

コントローラーを複製する

認証関連のコントローラーを、ユーザー用と管理者用の2つに複製します。

app/Http/Controllersに入っているAuthフォルダとProfileController.phpが認証関連のコントローラーです。Controllers直下にUserとAdminのフォルダを作って、その中にそれぞれ複製します。

Controllers直下に入っている認証関連のコントローラーは複製後削除します。

namespaceを合わせる

ファイルの階層が変わったので、各ファイルのnamespaceを下記コードのように変更します。Adminフォルダ内の場合は、Userの個所がAdminになります。

namespace App\Http\Controllers\Auth;
↓
namespace App\Http\Controllers\User\Auth;

viewのプレフィックスを合わせる

viewファイルもコントローラーと同じようにUserフォルダとAdminフォルダを作り、それぞれに認証関連のファイルを複製するので、コントローラー内のviewメソッドのプレフィックスを変更します。

user、adminをそれぞれ追加します。

return view('auth.login');
↓
return view('user.auth.login');

ガード処理を追加する

Authファサードにusersとadminsのガードを追加します。

Auth::logout();
↓
Auth::gurad('users')->logout();

webのガードが付いている個所もあるので、それはusersに変更します。

Auth::gurad('web')->logout();
↓
Auth::gurad('users')->logout();

リダイレクト先を変更する

リダイレクト先をuserとadminに変更します。

return redirect('/');
↓
return redirect('/user');

ルートメソッドで書かれている個所も変更します。

redirect()->route('login')…略…
↓
redirect()->route('user.login')…略…

管理者用のAdminフォルダに関しては、ルートサービスプロバイダーをADMIN_HOMEに変更します。

RouteServiceProvider::HOME;
↓
RouteServiceProvider::ADMIN_HOME;

RegisterdUserController.phpファイルは、Userモデルを使っているので管理者用のファイルはそこをAdminに変更します。

// app/Http/Controllers/Admin/Auth/RegisterdUserController.php

use App\Models\User;
↓
use App\Models\Admin;

return view('auth.register');
↓
return view('admin.auth.register');

'email' => ['required', 'string', 'email', 'max:255', 'unique:'.User::class],
↓
'email' => ['required', 'string', 'email', 'max:255', 'unique:'.Admin::class],

$user = User::create([
↓
$user = Admin::create([

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

return redirect(RouteServiceProvider::HOME);
↓
return redirect(RouteServiceProvider::ADMIN_HOME);

ブレードファイルを設定する

ブレードファイルを複製する

ブレードファイルもコントローラーと同じように、すでにある認証関連ファイルをユーザー用と管理者の2つに複製します。

resources/viewsに入っているauthフォルダが認証関連のブレードファイルです。views直下にuserとadminのフォルダを作って、その中にそれぞれ複製します。viewsフォルダ直下に入っているwelcome.blade.phpとdashboard.blade.phpも一緒に複製します。

コントローラーと同じように、複製完了後に元ファイルは削除します。

resources
    ∟views
        ∟user
            ∟auth
            dashboard.blade.php
            welcome.blade.php
        ∟admin
            ∟auth
            dashboard.blade.php
            welcome.blade.php

authファサードにガードを追加する

config/auth.phpファイルで設定したガードを、authファサードに追加します。ユーザー用(users)と管理者用(admins)をそれぞれ設定します。

@auth
↓
@auth('users')

ルートプレフィックスを追加する

ユーザー用(user)と管理者用(admin)のルートプレフィックスを追加します。

Route::has('login')
↓
Route::has('user.login')

route('login')
↓
route('user.login')

ダッシュボードのURLが記載してあるところは、管理者用のみadminを追加します。

url('/dashboard')
↓
url('/admin/dashboard')

navigationファイルを分岐させる

resources/views/layouts/navigation.blade.phpファイルに各ページへのリンクが書いてあります。これもユーザー用と管理者用で分ける必要があるので、ファイルを複製して中身のルートを変更します。

resources
    ∟views
        ∟layouts
            user-navibation.blade.php // navigation.blade.phpから複製
            admin-navibation.blade.php // navigation.blade.phpから複製
            navigation.blade.php // このファイルを削除
route('logout')
↓
route('user.logout')

request()->routeIs('dashboard')
↓
request()->routeIs('admin.dashboard')

resources/views/layouts/app.blade.phpファイルでnavigation.blade.phpを読み込んでいます。権限に応じて読み込むファイルを分岐する記述に変更します。

// resources/views/layouts/app.blade.php

…略…

<body class="font-sans antialiased">
    <div class="min-h-screen bg-gray-100">
        @include('layouts.navigation') // ここを削除

        // ここから追加
        @if(auth('admins')->user())
            @include('layouts.admin-navigation')
        @elseif(auth('users')->user())
            @include('layouts.user-navigation')
        @endif
        // ここまで追加

…略…

以上で、ユーザー権限と管理者権限でのマルチログイン機能は完了です。