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用を追加します。
config/session.php を修正します。
<?php
use Illuminate\Support\Str;
return [
︙
(中略)
︙
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
),
'cookie_admin' => Str::slug(env('APP_NAME', 'laravel'), '_').'_session_admin', // 追加
adminの場合は、上記で追加した設定を使用するようにします。
app/Providers/AppServiceProvider.php を修正します。
public function boot()
{
if (request()->is('admin/*')) {
config(['session.cookie' => config('session.cookie_admin')]);
}
}
コントローラーの作成
コントローラーを作成します。
デフォルトの認証機能をコピーして、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));
}