Webアプリケーションでは、ユーザー用の管理画面(いわゆるマイページ)と、管理者用の管理画面が別々に存在することも多いです。その場合、ログイン画面やDBテーブルも別になることが一般的でしょうか。
このように、1つのアプリケーションで個別の認証を持つことを、マルチ認証(Multi Auth)と言います。
今回はそんなマルチ認証を仕事で実装することになったのですが、機能はシンプル&低予算ということで、Breezeで実装することにしました。
実はこれまでも何度もやっていますが、その場対応でやっているので、何となくの理解です。加えて多くはLaravel9~10時代でした。
そこで今回はLaravel11×Breezeによる、マルチ認証の方法を書き留めておきたいと思います。
やりたいこと
あらためて現段階でやることを言語化してみます。
- ユーザー(
users
)と管理者(admins
)として別々のテーブルを持つ - 別々のモデルクラス、
User.php
,Admin.php
- デフォルトでは
/login
なのを、それぞれ/user/login
,/admin/login
としたい
とてもシンプルです。
手順としては、最初からあるBreezeのログインが/login
なのでそれを/user/login
とするところまで先に完了させます。その後、複製する形で管理者版を作る感じです。
ここまでをひな形として作っておけば、使い回しできそうです。
早速進めましょう。
はじめに
Laravelインストール
Laravelのインストールが出来ている状態からはじめます。
まだな方は以下をご参考ください。
Breezeインストール
Breezeも、以下のコマンドでインストール&初期化済みとします。
composer require laravel/breeze --dev
php artisan breeze:install
npm install
php artisan migrate
はまだしていません。
セッションをfileに
今回の案件は複数サーバー構成をとらないので、セッションはfile
で管理します。
laravel11はデフォルトでdatabase
になっているので、.env
にSESSION_DRIVER=file
を足します。
# ~略~
SESSION_DRIVER=file
# ~略~
ついでにDBの初期設定やタイムゾーン、メール設定など各自環境にあわせてやっておきます。
セッション管理にDBは使わないので、database\migrations\0001_01_01_000000_create_users_table.php
のsessions
テーブルは作らないでおきます。必要になった時に作れば良いかなと(もちろん今作っても可)。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', 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();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
// コメントアウト
// Schema::create('sessions', function (Blueprint $table) {
// $table->string('id')->primary();
// $table->foreignId('user_id')->nullable()->index();
// $table->string('ip_address', 45)->nullable();
// $table->text('user_agent')->nullable();
// $table->longText('payload');
// $table->integer('last_activity')->index();
// });
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
// コメントアウト
// Schema::dropIfExists('sessions');
}
};
ここでマイグレートします。
php artisan migrate
userディレクトリにファイルを移動
/user/login
などとしてアクセスできるように、既存のユーザー認証を/user
に全て移動します。
そのためにいろいろと設定やファイルを移動・整理します。
なぜ移動するのか?
ユーザーに関するBreezのファイルはそのままで、新規で/admin
だけ追加で作ってももちろん良いです。
ただ、個人的には/user
にまとめた方がスッキリするように思いました。
この作業が不要な方は行う必要はありませんが、今回は行う前提で進めます。
ビューの移動
ユーザー用のディレクトリ、resources\views\
にweb\user
ディレクトリを作ります。
作成したディレクトリの中に、以下のBreeze関連のディレクトリ、ファイルを放り込みます(=移動)。
resources\views\dashboard.php
-
resources\views\auth
resources\views\profile
加えてレイアウト関連も移動しましょう。
resources\views\layouts
にuser
ディレクトリを作ります。
作成したディレクトリの中に、以下のファイルを放り込みます。
- app.blade.php
- guest.blade.php
- navigation.blade.php
レイアウトファイルをユーザーと管理者で共有する場合は、やらなくてもOKです。
フォームリクエストの移動
ユーザー用のディレクトリ、app\Http\Requests\
にUser
ディレクトリを作ります。
作成したディレクトリの中に、以下のファイルを放り込みます。
app\Http\Requests\ProfileUpdateRequest.php
app\Http\Requests\Auth
こんな感じのディレクトリ構成になります。
ディレクトリ構成が変わるため、移動したファイルのnamespace
のパスを修正しておきます。
コントローラーの移動
まず、app\Http\Controllers\
にUser
ディレクトリを作ります。
そして作成したそのディレクトリに、既存の以下を放り込みます。
app\Http\Controllers\ProfileController.php
app\Http\Controllers\Auth
これでBreeze関連のファイルが、User
ディレクトリに完全に収まりました。
こちらも移動したファイルのnamespace
も併せて変えておきます。
app\Http\Controllers\User\Auth\
内のファイル例ですが、3行目を変えてます。
ついでに、先ほどフォームリクエストも移動しているので一式直しておきましょう(6行目)。
<?php
// 以下のようにUserが付く
namespace App\Http\Controllers\User\Auth;
use App\Http\Controllers\Controller;
// ↓Userディレクトリに移動したためこれも修正
use App\Http\Requests\User\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
app\Http\Controllers\User\ProfileController.php
だけは、さらに以下3行目のようにController
をuse
する必要がありますね。
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
class ProfileController extends Controller
{
ルート
auth.php
をweb_user.php
に変更し、web.php
も以下の様に整理しました。ここでコメントアウトした行は、この後web_user.php
に移動予定です。
<?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')->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 () {
require __DIR__ . '/web_user.php';
});
上記設定で、web_user.php
内の定義は強制的に/user
というプリフィクスがつくようになります。
そしてweb_user.php
では、guest
と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;
use Illuminate\Support\Facades\Route;
// 追加(パスにUserを追加)
use App\Http\Controllers\User\ProfileController;
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')->group(function () {
// プロフィール関連をweb.phpから移動
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::get('verify-email', EmailVerificationPromptController::class)
->name('verification.notice');
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
->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');
// dashboard関連をweb.phpから移動&修正
Route::middleware('verified')->group(function () {
Route::get('/dashboard', function () {
return view('web.user.dashboard');
})->name('dashboard');
// ここからverifiedなルートを追加可能
});
});
各種パスの変更
さて、ここまででユーザー関連のディレクトリに、ファイルを移動してきました。切りの良いタイミングですので、このあたりで動作確認をしてみます。
設定やルートをいじったので、キャッシュをクリアしておいた方が良いかもです。
php artisan config:clear
php artisan route:clear
各種パスを合わせる必要がある
人によってURLは変わると思いますが、例えばhttp://localhost/user/login
のようなログイン画面を表示してみます。
結論から言えば、以下の様に100%、Internal Server Errorとなります。
なぜなら、ファイルを移動しただけですので、コントローラ・ビュー(blade)内の各種パスがユーザー用に合っていないからです。
これらを改善していく必要があります。1つずつエラー内容を見ながら、修正していけば完了できるはずです。
……これがBreezeマルチ認証の最も面倒くさい部分なのですよね。
以下、私が行った内容をメモしておきます。
レイアウトファイルの修正
先ほどのエラーは、layout.guest
というビューが無いと言っています。
該当指定は、app\View\Components
の中にある、以下の2ファイルにより指定されています。
- AppLayout.php
- GuestLayout.php
上記ファイルを一括置換で以下に変更すれば、エラーが出なくなります。
return view('layouts.
→return view('layouts.user.
Bladeのルート・パス指定を修正
この状態でも、まだまだエラーが出ます。login
というルートは無いですよということです。
これは、ルート名にプリフィクスをつけたため、user.login
等としないといけないからです。
resource/views
の中にはこのような指定が数多くあります。これらも一式変更しなければなりません。
これまでと同じように、resource/views
内を対象にした検索置き換えで、以下のように一式置き換えます。
layouts.
→layouts.user.
route('
→route('user.
↑注:reset-password.blade.php
の$request->route('token')
だけは置き変えないrouteIs('
→outeIs('user.
Route::has('
→Route::has('user.
@include('profile
→@include('web.user.profile
……などなど、置き換えます。これでとりあえずはログイン画面が表示されるようになります。
置き換え多すぎ……。
コントローラーから読み込むビューの変更
まだまだやることがあります。
ログイン画面は表示されていますが、ダッシュボードを開こうとするとまだ同じエラーがでます。
次は、コントローラー内で指定するルート指定を変更することでエラーを解消します。
具体的には、app\Http\Controllers\User\
ディレクトリ内から読み込むビューファイルに、user
プリフィクスを付けます。ここでは2回、一括置換で済みます。
1つ目は、該当ディレクトリ内をview('
で検索し、view('web.user.
に置き換えます。以下の様に7ファイル存在しました。
※初稿ではこちらが最初の置き換えだったのでスクショがありますが気にしないでください。
view
メソッド以外に、route
メソッドでもビューの指定がありますので置き換えなければなりません。
これが2つ目で、先ほどと同一のディレクトリを対象に、以下を置き換えます。
route('
→route('user.
パスワードリセット
これで一見できたかのように見えますが、まだまだやることはたくさんあります。
パスワードをリセットするページhttp://localhost/user/forgot-password
からリセットをしようとするとエラーが生じています。
パスワードリセットの処理は、以下のファイルに書かれています。
vendor\laravel\framework\src\Illuminate\Auth\Notifications\ResetPassword.php
protected function resetUrl($notifiable)
{
if (static::$createUrlCallback) {
return call_user_func(static::$createUrlCallback, $notifiable, $this->token);
}
return url(route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
}
フレームワーク内でpassword.reset
が指定されてしまうので、問題が生じているわけですね。
このファイルを修正するのはもちろんよろしくありません。ですのでこれを継承したクラスを作成&利用します。
以下のコマンドを打ち込みます。
php artisan make:notification CustomResetPassword
ファイルが出来るので、以下の様に修正します。
<?php
namespace App\Notifications;
// use Illuminate\Bus\Queueable;
// use Illuminate\Contracts\Queue\ShouldQueue;
// use Illuminate\Notifications\Messages\MailMessage;
// use Illuminate\Notifications\Notification;
// 追加
use Illuminate\Auth\Notifications\ResetPassword as BaseResetPassword;
class CustomResetPassword extends BaseResetPassword
{
// use Queueable;
/**
* コンストラクタを置く場合は
* 親のコンストラクタを呼ばないとエラーになるので
* コメントアウトしておく
*/
// public function __construct()
// {
//
// }
/**
* Get the reset URL for the given notifiable (user or admin).
*
* @param mixed $notifiable
* @return string
*/
protected function resetUrl($notifiable)
{
// 管理者の場合は admin のルート、それ以外のユーザーの場合は user のルートを使用
if (request()->routeIs('admin.*')) {
return url(route('admin.password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
} else {
return url(route('user.password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
}
}
}
そしてapp/Models/User.php
モデルで、それを使用するようにします。
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
// 追加
use App\Notifications\CustomResetPassword;
class User extends Authenticatable
{
//(略)
// パスワードリセット通知のカスタマイズ
public function sendPasswordResetNotification($token)
{
$this->notify(new CustomResetPassword($token));
}
//(略)
}
これでパスワードのリセットができるようになりました。
ログインリダイレクト
表面化しづらいですが、まだ問題があります。まだログインされていない状態に、ダッシュボードhttp://localhost/user/dashboard
へリクエストするとエラーになります。
このエラーで指定されているルート:login
は、以下で定義されています。
vendor\laravel\framework\src\Illuminate\Foundation\Configuration\ApplicationBuilder.php
<?php
namespace Illuminate\Foundation\Configuration;
use Closure;
use Illuminate\Console\Application as Artisan;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
use Illuminate\Contracts\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Bootstrap\RegisterProviders;
use Illuminate\Foundation\Events\DiagnosingHealth;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as AppEventServiceProvider;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as AppRouteServiceProvider;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Laravel\Folio\Folio;
class ApplicationBuilder
// (略)
public function withMiddleware(?callable $callback = null)
{
$this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) {
$middleware = (new Middleware)
->redirectGuestsTo(fn () => route('login'));
if (! is_null($callback)) {
$callback($middleware);
}
こちらもフレームワーク内のファイルを直接修正するわけにはいかないので、アプリケーション内で対策します。
これはミドルウェアで対処することになります。
ミドルウェアは、Laravel11からはbootstrap/app.php
を編集します。Adminはまだ作っていませんが、その処理もついでに書いておきましょう。
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
// 追加
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// リクエストルートが admin.* なら admin.login に、それ以外なら user.login にリダイレクト
$middleware->redirectGuestsTo(function (Request $request) {
return $request->routeIs('admin.*')
? route('admin.login')
: route('user.login');
});
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
これで、未認証時にログイン画面にリダイレクトされるようになりました。
ちなみにLaravel10までは、app\Http\Middleware\Authenticate.php
に書けば良いです。
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
if ($request->expectsJson()) {
return null;
}
if ($request->routeIs('admin.*')) {
return route('admin.login');
}
if ($request->routeIs('user.*')) {
return route('user.login');
}
return route('login');
}
}
メールアドレス認証
Webアプリケーションでは、メールアドレス認証をする、つまりUser
モデルがMustVerifyEmail
を実装する場合も多いですよね。
以下の様な感じです。
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use App\Notifications\CustomResetPassword;
class User extends Authenticatable implements MustVerifyEmail
{
その場合、ユーザー登録時に以下の様にエラーになります。
こちらもフレームワーク内にルートverification.verify
の指定があります。
vendor\laravel\framework\src\Illuminate\Auth\Notifications\VerifyEmail.php
これも、自分で通知クラスを作りオーバーライドします。
以下のコマンドで通知クラスを作成します。
php artisan make:notification CustomVerifyEmail
コマンドにより作られたapp\Notifications\CustomVerifyEmail.php
を、以下のように編集します。
<?php
namespace App\Notifications;
use Illuminate\Auth\Notifications\VerifyEmail as BaseVerifyEmail;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
class CustomVerifyEmail extends BaseVerifyEmail
{
protected function verificationUrl($notifiable)
{
if (static::$createUrlCallback) {
return call_user_func(static::$createUrlCallback, $notifiable);
}
// 管理者かユーザーかでルート名を切り替え
$routeName = request()->routeIs('admin.*')
? 'admin.verification.verify'
: 'user.verification.verify';
return URL::temporarySignedRoute(
$routeName, // ルート名を動的に設定
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
]
);
}
}
上記修正したファイルを、app/Models/User.php
で利用します。
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use App\Notifications\CustomResetPassword;
// 追加
use App\Notifications\CustomVerifyEmail;
class User extends Authenticatable implements MustVerifyEmail
{
//(略)
// メール認証通知のカスタマイズ
public function sendEmailVerificationNotification()
{
$this->notify(new CustomVerifyEmail());
}
//(略)
}
ミドルウェア
verification.verify
のエラーは解消しましたが、メールアドレス認証が済んでいない状態でダッシュボードにアクセスしようとすると、verification.notice
のエラーが発生します。
これは、vendor\laravel\framework\src\Illuminate\Auth\Middleware\EnsureEmailIsVerified.php
にて指定されています。以下のミドルウェアを作ります。
php artisan make:middleware CustomEnsureEmailIsVerified
app\Http\Middleware\CustomEnsureEmailIsVerified.php
が作られるので、以下の様にコードを修正します。
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\URL;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified as BaseEnsureEmailIsVerified;
// 追加
use Illuminate\Contracts\Auth\MustVerifyEmail;
class CustomEnsureEmailIsVerified extends BaseEnsureEmailIsVerified{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param string|null $guard
* @param string|null $redirectToRoute
*/
public function handle($request, Closure $next, $guard = null, $redirectToRoute = null)
{
$user = auth()->guard($guard)->user();
if (! $user || ($user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail())) {
$route = $redirectToRoute;
if ($guard === 'admin') {
$route = $redirectToRoute ?: 'admin.verification.notice';
} else {
$route = $redirectToRoute ?: 'user.verification.notice';
}
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::guest(URL::route($route));
}
return $next($request);
}
}
そしてこのミドルウェアをverified
として登録することで、デフォルトの設定を上書きします。
bootstrap/app.php
を以下の様に編集します。
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__ . '/../routes/web.php',
commands: __DIR__ . '/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// リクエストルートが admin.* なら admin.login に、それ以外なら user.login にリダイレクト
$middleware->redirectGuestsTo(function (Request $request) {
return $request->routeIs('admin.*')
? route('admin.login')
: route('user.login');
});
// エイリアスとして追加
$middleware->alias([
'verified' => \App\Http\Middleware\CustomEnsureEmailIsVerified::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
これでメールアドレス認証がただしく動作するようになりました。
ログイン時のリダイレクト
Laravelでは、ログイン済みの場合に特定の画面にリダイレクトするミドルウェアがあります。
例えばログイン後にhttp://localhost/user/login
などにアクセスすると、通常ダッシュボードに移動するのですが、現在はトップ(http://localhost/
)に移動するようです。
これもきちんと設定しておかなければ不親切なサイトになってしまいます。
これはguest
として\Illuminate\Auth\Middleware\RedirectIfAuthenticated.php
が割り当てられているためです。独自ミドルウェアを作り上書きしてあげます。
以下のコマンドでミドルウェアを作成します。
php artisan make:middleware CustomRedirectIfAuthenticated
app\Http\Middleware\CustomRedirectIfAuthenticated.php
ができますので、私は以下の様に、defaultRedirectUri()
メソッドをオーバーライドして実現してみました。
<?php
namespace App\Http\Middleware;
// 追加
use Illuminate\Auth\Middleware\RedirectIfAuthenticated as BaseRedirectIfAuthenticated;
class CustomRedirectIfAuthenticated extends BaseRedirectIfAuthenticated
{
/**
* Get the default URI the user should be redirected to when they are authenticated.
*/
protected function defaultRedirectUri(): string
{
if (request()->routeIs('admin.*')) {
return route('admin.dashboard');
}
return route('user.dashboard');
}
}
bootstrap/app.php
から登録します。
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__ . '/../routes/web.php',
commands: __DIR__ . '/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// リクエストルートが admin.* なら admin.login に、それ以外なら user.login にリダイレクト
$middleware->redirectGuestsTo(function (Request $request) {
return $request->routeIs('admin.*')
? route('admin.login')
: route('user.login');
});
$middleware->alias([
'verified' => \App\Http\Middleware\CustomEnsureEmailIsVerified::class,
// 追加
'guest' => \App\Http\Middleware\CustomRedirectIfAuthenticated::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
これで正しくリダイレクトするようになりました。
これでひとまずユーザーは完了です!
Adminを作る
ユーザー関連を作りつつ、ミドルウェア等での切り分け処理もついでに書いてきたので、あとは簡単です。
Adminモデルを作る
\App\Models\User
をコピーし、\App\Models\Admin
を作っておきます。
adminガードを作る
config\auth.php
を編集して、admin
ガードを作ります。
// ~(略)~
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// 追加
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
],
// ~(略)~
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
// 追加
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
],
// ~(略)~
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
// 追加
'admins' => [
'provider' => 'admins',
// セキュリティは別として、password_reset_tokensと共用でも動きはする模様
'table' => 'admin_password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
フォームリクエストを複製する
app\Http\Requests\User
ディレクトリを複製します。
そして、コントローラーの時と同様にいろいろ置換します。
意図しない置換を防ぐために、大文字小文字を区別するチェックをいれてから実施するのが良いでしょう。
User
→Admin
user()
→user('admin')
Auth::attempt
→Auth::guard('admin')->attempt
Controllerを複製する
app\Http\Controllers\User
ディレクトリをコピーし、Admin
ディレクトリを作ります。
名称やuse
などのパスも全てAdmin
に変更する必要があります。
以下が置き換え参考例です。
User
→Admin
guard('web')
→guard('admin')
$request->user()
→$request->user('admin')
route('user.
→route('admin.
view('web.user.
→view('web.admin.
Auth::logout()
→Auth::guard('admin')->logout()
Auth::login(
→Auth::guard('admin')->login(
ここで重要なのですが、以下のファイルがあります。
app\Http\Controllers\Admin\Auth\RegisteredUserController.php
一括置き換えでUser
をAdmin
に変更すると、このファイルだけ、ファイル名とクラス名が合わなくなります。このファイル名はUser
のままでいいやという方は、手作業で戻してください。
もしくは、Admin
にしたいという場合はファイル名をRegisteredAdminController.php
に変更してください。
私はファイル名を変えないで進めます。
ビューコンポーネントを複製
app\View\Components
にあるファイルをそれぞれ以下の様な名称で複製します。
AppLayout.php
→AdminAppLayout.php
GuestLayout.php
→AdminGuestLayout.php
クラス名やviewメソッドのパラメータも管理者用に変えます。
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class AdminAppLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.admin.app');
}
}
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class AdminGuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.admin.guest');
}
}
ビューレイアウトを複製
resources\views\layouts
にあるuser
を複製し、admin
を作ります。
同様にディレクトリ内を以下で置き換えます。
user.
→admin.
ビューを複製する
resources\views\web\user
を複製してadmin
ディレクトリを作ります。
あとは、resources\views\web\admin
ディレクトリ内を以下で置き換えます。
user.
→admin.
x-app-layout
→x-admin-app-layout
x-guest-layout
→x-admin-guest-layout
マイグレーションファイルを複製する
database\migrations\0001_01_01_000000_create_users_table.php
を参考に、管理者用のマイグレーションファイルを作っておきます。
上記ファイルを複製し、0001_01_03_000000_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
{
/**
* Run the migrations.
*/
public function up(): void
{
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();
});
Schema::create('admin_password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
// Schema::create('admin_sessions', function (Blueprint $table) {
// $table->string('id')->primary();
// $table->foreignId('user_id')->nullable()->index();
// $table->string('ip_address', 45)->nullable();
// $table->text('user_agent')->nullable();
// $table->longText('payload');
// $table->integer('last_activity')->index();
// });
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
// Schema::dropIfExists('sessions');
}
};
終わったら、以下のコマンドでマイグレートしておきます。
php artisan migrate
ルートを作る
routes\web_user.php
を複製して、routes\web_admin.php
を作り、ユーザー用になっているのを、各種置き換えます。
User
→Admin
user
→admin
guest
→guest:dmin
auth
→auth:admin
verified
→verified: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::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 () {
// プロフィール関連をweb.phpから移動
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::get('verify-email', EmailVerificationPromptController::class)
->name('verification.notice');
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
->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');
// dashboard関連をweb.phpから移動&修正
Route::middleware('verified:admin')->group(function () {
Route::get('/dashboard', function () {
return view('web.admin.dashboard');
})->name('dashboard');
// ここからverifiedなルートを追加可能
});
});
ここもRegisteredUserController
をUser
のままにした場合は置き換え注意です。一括置き換えできなくて面倒なので、RegisteredAdminController
にファイル名を変更しておく手もありますね。
いましがた作ったファイルを、routes\web.php
に追加します。
<?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')->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 () {
require __DIR__ . '/web_user.php';
});
Route::prefix('admin')->name('admin.')->group(function () {
require __DIR__ . '/web_admin.php';
});
その他問題等
パスワードのテーブルがusersが使われてしまう問題
app\Http\Controllers\Admin\Auth\PasswordResetLinkController.php
を以下のように置き換えます。
Password::sendResetLink
→Password::broker('admins')->sendResetLink
Password::reset
→Password::broker('admins')->reset
まとめ
いやー、面倒臭い。記事にするのは大変でした。これを書くのに、4~5回ほど最初からやり直したくらいです。
Laravel側で、やりやすい方法を用意してくれていたらいいんですけれどね。どうやら推奨されていないっぽくて、期待薄です。
間違い等がある可能性や、後から問題が出る可能性もあると思いますので、適宜加筆・修正したいと思います。
今回は私の例でしたが、参考になるところがありましたら幸いです。
くれぐれも、置き換えは慎重に。ディレクトリの対象範囲や、文字を誤って置き換えると、ごちゃごちゃになって、大変です(経験談)。
コメント