Laravelのマルチログインを図解しながら実装

シェアしてね〜🤞

LaravelのBreezeを利用して、マルチログインを実装していきます。

すでにBreezeを使ったユーザーログインができることを前提で話をすすめていきます。まだユーザーログインが実装出来ていない場合は、前回の記事を参考に実装しておいてください。 Laravel Breezeで「ユーザー登録・ログイン」認証機能を作る!

実装する環境としては以下のとおりです。

  • Laravel 10.x
  • PHP 8.2

マルチログインということで、ユーザー認証に加えて、管理者(Admin)という概念を加えて管理者用のログインを実装していきます。

なので今回のマルチログインでは、以下の2つの認証が存在することになります。

  • ユーザーログイン
  • 管理者ログイン

それぞれ別のテーブルで、それぞれの人を管理していくイメージです。

今回作成するマルチログインの構成について

まずマルチログインを実装するにあたって、先にどのような構成、画面イメージなるか図で示しておきます。

ユーザー認証に加えてファイルが増えるので、全体のリクエストイメージや画面イメージを最初に抑えておきましょう。

ここで何を作るべきなのかをしっかりイメージしておきます。

以下はユーザーと管理者それぞれで、ログインを実行した場合のリクエストフローとその時に使用されるファイルです。

Frame 1 (1)

ユーザーと管理者で同じようなフローを通ってログインすることになります。上半分の青い部分がユーザー、下半分の赤い部分が管理者です。

ユーザーログインの実装はすでにあるので、赤い部分だけを実装していくイメージです。

上の図にあるように、

  • ルーティング
  • コントローラ
  • モデル
  • データ

この辺を作っていけば良さそうです。マルチログインなので、もちろんログインエンドポイントは別れます。管理者のログインは/admin/loginにPOSTすることにします。

Breezeではユーザー認証周りを実装したときは、初期ファイル生成で上記のファイルが一気に作られますが、管理者でも同様のファイルを作成していけば良いだけです。マルチログイン実装では、追加で必要になるファイルもあるので、追って解説します。

次に、画面イメージを見ておきましょう。最終的に以下のような画面になります。

Frame 3

上半分はユーザーの「登録・ログイン・ダッシュボード」の画面です。これは通常のBreezeで手に入る画面です。

今回は下半分の管理者の「登録・ログイン・ダッシュボード」を作成していきます。管理者用の各種画面は/adminから始まるURLとしました。

データとadminモデル作成

ではまずは管理者のデータ(テーブル)とモデルを作成していきます。

図でいうと以下を作っていきます。

AuthFlow2

まずはadminsテーブルのマイグレーションファイル作成をします。usersテーブルと同じなのでコピーしてテーブル名だけ変更すれば良いです。

以下のようになります。

database/migrations/2023_02_18_030304_create_admins_table.php

<?php

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

return new class extends Migration
{
    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();
        });
    }

    public function down()
    {
        Schema::dropIfExists('admins');
    }
};

上記をコピーしてもいいですが、以下コマンドでマイグレーションファイルを作成できます。

docker compose exec laravel.test php artisan make:migration create_admins_table

マイグレーションファイルが作成できたら、以下を実行してDBにテーブル投入しておきましょう。

docker compose exec laravel.test php artisan migrate

次にAdminモデルを作成します。こちらもUserモデルと同じです。

app/Models/Admin.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;

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

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

auth.phpのガードとプロバイダ設定

次はauth.phpという設定を編集していきます。

Laravelでマルチログインを実装する上で一番ここが重要です。設定項目は少ないですが詳しく見ていきましょう。

config/auth.phpに管理者用の記述をすることで、ユーザー・管理者のマルチログインが可能になります。(これは前述の図には表していない部分です)

変更がない部分は省略しています。

config/auth.php

    // 省略

    '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,
        ],
    ],

        // 省略

guardsとprovidersの箇所にadminの設定を追加しています。

追加した上記2つについて簡単に説明します。 

config/auth.phpのguardsとは?

guardsは認証機能を提供するものです。guardsは複数指定することができ、これの単位でアプリケーション側で認証する対象を複数使えるようになります。

webは一般ユーザーを表しています(名前は変更可能です)。今回は管理者を追加したかったので、adminとしてあげることで、一般のユーザーとは別に管理者の認証を提供することができるようになります。

これをすることでLaravel上でユーザーはユーザーとして、管理者は管理者として区別することが可能になります。

guardsのdriverについて

これは認証した情報をどのように管理するかという方法を設定する箇所です。

sessionとしているのは、よくあるcookie/session方式でユーザーのログイン状態を管理しています。

ここにはjwtなど他の管理方法などが設定可能です。今回はスタンダードなsessionとしています。

guardsのproviderについて

これは「どのような仕組み」で「どの認証対象」を認証するかを指定するものです。

config/auth.phpのprovidersを指定します。

config/auth.phpのprovidersとは?

これが先程guardsのproviderとして指定するものになります。

ここではまた2つの設定があり、それを使って以下を指定していきます。

  • 「どのような仕組みで認証するのか」
  • 「どの認証対象」
providersのdriverについて

今回はeloquentを指定しています。

UserやAdminモデルでは上記のような記述があり、Illuminate\Foundation\Auth\Userをモデルのクラスにextendsすることで、eloquent経由(UserやAdminクラス経由)で認証することが可能になります。

use Illuminate\Foundation\Auth\User as Authenticatable;

この他にdriverはdatabaseなどがあり、これを指定するなら独自で認証する仕組みを実装する必要があります。リクエストされたメールアドレスとパスワードとDBに入ってるユーザーとを検証して、諸々一致していればセッション発行してログインさせる感じです。

providersのmodelについて

これが「どの認証対象」を指定する場所です。

User.phpやAdmin.phpを指定します。

providersのdriverでeloquentをしているので

Illuminate\Foundation\Auth\User

をextendsする必要があります。

ビュー、コントローラ、ルーティングの作成

追加以下の場所をさくっと作っていきましょう。

AuthFlow2 (1)

ビューの作成

管理者用のビューは図の通り3つ作成します。

スクリーンショット 2023-02-23 10.36.45

  • 登録ページ
  • ログインページ
  • ダッシュボード(ログイン後)

ほとんど既存のユーザー用のページと一緒です。エンドポイントやリンクが変わっているくらいです。

登録ページ

resources/views/admin/auth/register.blade.php

<x-guest-layout>
    <h1 class="text-white">Adminの登録</h1>
    <form method="POST" action="{{ route('admin.register') }}">
        @csrf

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

        <!-- Email Address -->
        <div class="mt-4">
            <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 autocomplete="username" />
            <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="new-password" />

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

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

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

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

        <div class="flex items-center justify-end mt-4">
            <a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('login') }}">
                {{ __('Already registered?') }}
            </a>

            <x-primary-button class="ml-4">
                {{ __('Register') }}
            </x-primary-button>
        </div>
    </form>
</x-guest-layout>
ログインページ

resources/views/admin/auth/login.blade.php

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

    <h1 class="text-white">Adminのログイン</h1>
    <form method="POST" action="{{ route('admin.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 autocomplete="username" />
            <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 dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember">
                <span class="ml-2 text-sm text-gray-600 dark:text-gray-400">{{ __('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 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('password.request') }}">
                    {{ __('Forgot your password?') }}
                </a>
            @endif

            <x-primary-button class="ml-3">
                {{ __('Log in') }}
            </x-primary-button>
        </div>
    </form>
</x-guest-layout>
ダッシュボード(ログイン後)

resources/views/admin/dashboard.blade.php

<x-app-layout>

    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
            Adminのダッシュボード
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 text-gray-900 dark:text-gray-100">
                    <h1 class="text-white">Adminのダッシュボード</h1>
                    {{ __("You're logged in!") }}

                    <div>{{ Auth::user()->name }}</div>

                </div>
            </div>
        </div>
    </div>
</x-app-layout>

ダッシュボードではユーザーと同じレイアウトを使っているので画面右上に管理者名が表示されるんですが、以下の記述でログイン中の管理者名が参照できるので、ダッシュボード中央にも同じ記述をしておきました。

<div>{{ Auth::user()->name }}</div>

これでビューファイルの作成は終わりです。

コントローラの作成

作成するコントローラファイルは2つです。

スクリーンショット 2023-02-23 10.48.27

各コントローラで何をするかは以下です

  • 管理者登録コントローラ
    • 登録画面呼び出し
    • 登録実行
  • ログインコントローラ
    • ログイン画面呼び出し
    • ログイン実行
管理者登録コントローラ

app/Http/Controllers/Admin/AdminRegisterController.php

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Admin;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;

class AdminRegisterController extends Controller
{
    // 登録画面呼び出し
    public function create(): View
    {
        return view('admin.auth.register');
    }

    // 登録実行
    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()],
        ]);

        $admin = Admin::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        event(new Registered($admin));

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

        return redirect(RouteServiceProvider::HOME);
    }
}

以下のようにAuth::guard('admin')とすることで、管理者としてログインを試行することが出来ます。

Auth::guard('admin')->login($admin);
ログインコントローラ

app/Http/Controllers/Admin/AdminLoginController.php

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;

class AdminLoginController extends Controller
{
    // ログイン画面呼び出し
    public function showLoginPage(): View
    {
        return view('admin.auth.login');
    }

    // ログイン実行
    public function login(LoginRequest $request): RedirectResponse
    {
        $credentials = $request->only(['email', 'password']);

        if (Auth::guard('admin')->attempt($credentials)) {
            return redirect()->route('admin.dashboard')->with([
                'login_msg' => 'ログインしました。',
            ]);
        }

        return back()->withErrors([
            'login' => ['ログインに失敗しました'],
        ]);
    }
}

ちなみにダッシュボードの画面はルーティングから直接呼び出しています。画面を表示させるだけだったらログイン画面も登録画面もそれでも良いです。今回はコントローラに書きましたが。

ルーティングの作成

ルーティングの作成を行なって終わりです。

必要クラスをuseしてファイル末尾に管理者用のルーティングを記述していきましょう。

routes/web.php

use App\Http\Controllers\Admin\AdminLoginController;
use App\Http\Controllers\Admin\AdminRegisterController;

// 省略

/*
|--------------------------------------------------------------------------
| 管理者用ルーティング
|--------------------------------------------------------------------------
*/
Route::group(['prefix' => 'admin'], function () {
    // 登録
    Route::get('register', [AdminRegisterController::class, 'create'])
        ->name('admin.register');

    Route::post('register', [AdminRegisterController::class, 'store']);

    // ログイン
    Route::get('login', [AdminLoginController::class, 'showLoginPage'])
        ->name('admin.login');

    Route::post('login', [AdminLoginController::class, 'login']);

    // 以下の中は認証必須のエンドポイントとなる
    Route::middleware(['auth:admin'])->group(function () {
        // ダッシュボード
        Route::get('dashboard', fn() => view('admin.dashboard'))
            ->name('admin.dashboard');
    });
});

ログインしてないと見せたくないページやエンドポイントなどは、以下のミドルウェア内に記述していけばOKです。

    Route::middleware(['auth:admin'])->group(function () {
        // ここに記述
    });

画面イメージ

今回ログアウトは実装してないので、ログアウトはできません。ログインとかと同じように作成していけば良いので、試してみてください。

image

こんな感じのダッシュボードに最終的にアクセスできれば成功です。 

追加で別のログインを実装するには

今回は管理者を追加してマルチログインを実装しました。今回のケース以外にも例えば、

クラウドソーシングサイトなどを考えた場合、クライアントとワーカー、管理者など3つのログインが必要になるかと思います。

このような場合にも、今回実装してきたみたいに、auth.phpにguardsやprovidersを追加して、各種ビュー、モデル、コントローラ、ルーティングなどを実装していけば良いです。一度この仕組を作ってしまえば、増やすのは簡単です。

今回のマルチログインの内容はこれで以上です。

シェアしてね〜🤞