やること
-
ユーザーのログイン認証を作成
-
管理のログイン認証を作成
-
ログイン状態をsessionsテーブルでそれぞれ独立して管理できるように対応
環境
- OS
- Debian GNU/Linux 12 (bookworm)
- PHP
- 8.3.10
- docker
参考
【Laravel】マルチログイン機能を作ってみる(Laravel11, 10)
Laravel マルチログインでセッションをDB管理する方法
プロジェクト作成
composer create-project laravel/laravel laravel_web_multi_auth
起動
php artisan serve
dockerを使用している場合
php artisan serve --host=0.0.0.0
ユーザーの認証
ユーザーの認証を作成し、動作確認後にアドミンの認証を作成します。
seeder
- database/seeders/DatabaseSeeder.php
+use DateTime;
+use Illuminate\Support\Facades\Hash;
/*** 略 ***/
public function run(): void
{
- User::factory()->create([
- 'name' => 'Test User',
- 'email' => 'test@example.com',
- ]);
+ if (app()->isLocal()) {
+ // 開発環境のみ
+ User::factory()
+ ->count(10)
+ ->sequence(function ($sequence) {
+ return [
+ 'name' => sprintf('user_%02d', $sequence->index + 1),
+ 'email' => sprintf('user_%02d@example.com', $sequence->index + 1),
+ 'password' => Hash::make(sprintf('user_%02d_password', $sequence->index + 1)),
+ 'created_at' => new DateTime(),
+ 'updated_at' => new DateTime(),
+ ];
+ })
+ ->create();
+ }
}
seeder実行
php artisan db:seed
コントローラー作成
php artisan make:controller UserLoginController
- app/Http/Controllers/UserLoginController.php
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UserLoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class UserLoginController extends Controller
{
/**
* ログイン画面
*/
public function create(): View {
return view('user.login');
}
/**
* ログイン
*/
public function store(UserLoginRequest $request): RedirectResponse {
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('user.top'));
}
/**
* ログアウト
*/
public function destroy(Request $request): RedirectResponse {
$request->session()->invalidate();
$request->session()->regenerateToken();
return to_route('user.login');
}
}
リクエスト作成
php artisan make:request UserLoginRequest
- app/Http/Requests/UserLoginRequest.php
public function authorize(): bool
{
- return false;
+ return true;
}
/*** 略 ***/
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
+ 'email' => ['required', 'email'],
+ 'password' => 'required',
];
}
+ public function attributes(): array
+ {
+ return [
+ 'email' => 'E-mail',
+ 'password' => 'Password',
+ ];
+ }
+
+ public function messages(): array
+ {
+ return [
+ 'email.required' => ':attributeを入力してください',
+ 'email.email' => ':attributeが正しくありません',
+ 'password.required' => ':attributeを入力してください',
+ ];
+ }
+
+ public function authenticate(): void
+ {
+ // attemptでログインを試みる
+ if (!Auth::attempt($this->only(['email', 'password']))) {
+ throw ValidationException::withMessages(['failed' => __('auth.failed')]);
+ }
+ }
}
ミドルウェア
- bootstrap/app.php
+use Illuminate\Http\Request;
/*** 略 ***/
->withMiddleware(function (Middleware $middleware) {
+ $middleware->redirectGuestsTo(function(Request $request) {
+ if (request()->routeIs('user.*')) {
+ return $request->expectsJson() ? null : route('user.login');
+ }
+ return $request->expectsJson() ? null : route('auth');
+ });
})
ルーティング
- routes/web.php
+use App\Http\Controllers\UserLoginController;
/*** 略 ***/
+// ユーザーログイン画面
+Route::get('/user-login', [UserLoginController::class, 'create'])->name('user.login');
+// ユーザーログイン
+Route::post('/user-login', [UserLoginController::class, 'store'])->name('user.login.store');
+// ユーザーログアウト
+Route::delete('/user-login', [UserLoginController::class, 'destroy'])->name('user.login.destroy');
+
+// ユーザーログイン後のみアクセス可
+Route::middleware('auth:web')->group(function () {
+ Route::get('/user-top', function () {
+ return view('user.top');
+ })->name('user.top');
+});
ビュー
ログインView
php artisan make:view user/login
- resources/views/user/login.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ユーザー</title>
<style>
html,
body {
height: 100%;
}
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
li {
list-style: none;
}
form {
text-align: right;
}
.alert {
width: 300px;
padding: 8px;
}
.alert ul {
padding: 0;
}
.alert ul li {
color: red;
text-align: left;
overflow-wrap: break-word;
}
</style>
</head>
<body>
<main>
<h2>ユーザーログイン</h2>
<form method="POST" action="{{ route('user.login.store') }}">
@csrf
<div>
<label for="email">E-main: </label>
<input type="text" id="email" name="email" required>
</div>
<div>
<label for="password">Password: </label>
<input type="password" id="password" name="password" required>
</div>
<div class="alert">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
<button type="submit">ログイン</button>
</form>
</main>
</body>
</html>
トップView
php artisan make:view user/top
- resources/views/user/top.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ユーザー</title>
<style>
html,
body {
height: 100%;
}
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
form {
text-align: right;
}
</style>
</head>
<body>
<main>
<h2>ユーザートップ</h2>
@auth('web')
<p>ログイン中</p>
@endauth
<form method="POST" action="{{ route('user.login.destroy')}}">
@method('DELETE')
@csrf
<button type="submit">ログアウト</button>
</form>
</main>
</body>
</html>
ここまで実装するとユーザー認証ができると思います。
管理の認証
マルチ認証するにあたって対応すること
- テーブルのsessions(ログイン状態管理)をユーザーと管理でそれぞれ保存できるように対応する
モデル作成
php artisan make:model Admin -mf
オプションのm
はマイグレーションファイル、f
はファクトリーを作成
- app/Models/Admin.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
-use Illuminate\Database\Eloquent\Model;
+use Illuminate\Foundation\Auth\User as Authenticatable;
-class Admin extends Model
+class Admin extends Authenticatable
{
use HasFactory;
}
- database/migrations/YYYY_MM_DD_hhmmss_create_admins_table.php
管理IDもテーブル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('admins', function (Blueprint $table) {
$table->id();
+ $table->string('email')->unique()->comment('E-mail');
+ $table->string('name')->comment('名前');
+ $table->string('password')->comment('パスワード');
$table->timestamps();
});
+ Schema::table('sessions', function (Blueprint $table) {
+ $table->foreignId('admin_id')->after('user_id')->nullable()->index();
+ });
}
/**
* Reverse the migrations.
*/
public function down(): void
{
+ Schema::table('sessions', function (Blueprint $table) {
+ $table->dropIndex('sessions_admin_id_index');
+ $table->dropColumn('admin_id');
+ });
Schema::dropIfExists('admins');
}
};
- マイグレート実行
php artisan migrate
seeder
- database/seeders/DatabaseSeeder.php
+use App\Models\Admin;
/*** 略 ***/
if (app()->isLocal()) {
// 開発環境のみ
User::factory()
->count(10)
->sequence(function ($sequence) {
return [
'name' => sprintf('user_%02d', $sequence->index + 1),
'email' => sprintf('user_%02d@example.com', $sequence->index + 1),
'password' => Hash::make(sprintf('user_%02d_password', $sequence->index + 1)),
'created_at' => new DateTime(),
'updated_at' => new DateTime(),
];
})
->create();
+ // 管理
+ Admin::factory()
+ ->count(10)
+ ->sequence(function ($sequence) {
+ return [
+ 'name' => sprintf('admin_%02d', $sequence->index + 1),
+ 'email' => sprintf('admin_%02d@example.com', $sequence->index + 1),
+ 'password' => Hash::make(sprintf('admin_%02d_password', $sequence->index + 1)),
+ 'created_at' => new DateTime(),
+ 'updated_at' => new DateTime(),
+ ];
+ })
+ ->create();
}
- シーダー再登録のためマイグレートリフレッシュ
php artisan migrate:refresh --seed
コントローラー作成
php artisan make:controller AdminLoginController
- app/Http/Controllers/AdminLoginController.php
<?php
namespace App\Http\Controllers;
use App\Http\Requests\AdminLoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AdminLoginController extends Controller
{
/**
* ログイン画面
*/
public function create(): View
{
return view('admin.login');
}
/**
* ログイン
*/
public function store(AdminLoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
// sessionにurl.intendedがセットされていたら、その値へリダイレクトする
// ない場合はadmin.topにリダイレクトする
return redirect()->intended(route('admin.top'));
}
/**
* ログアウト
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('admin')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return to_route('admin.login');
}
}
リクエスト作成
php artisan make:request AdminLoginRequest
- app/Http/Requests/AdminLoginRequest.php
public function authorize(): bool
{
- return false;
+ return true;
}
/*** 略 ***/
public function rules(): array
{
return [
+ 'email' => ['required', 'email'],
+ 'password' => 'required',
];
}
+ public function attributes(): array
+ {
+ return [
+ 'email' => 'E-mail',
+ 'password' => 'Password',
+ ];
+ }
+
+ public function messages(): array
+ {
+ return [
+ 'email.required' => ':attributeを入力してください',
+ 'email.email' => ':attributeが正しくありません',
+ 'password.required' => ':attributeを入力してください',
+ ];
+ }
+
+ public function authenticate(): void
+ {
+ // attemptでログインを試みる
+ if (!Auth::guard('admin')->attempt($this->only(['email', 'password']))) {
+ throw ValidationException::withMessages(['failed' => __('auth.failed')]);
+ }
+ }
}
ミドルウェア
- bootstrap/app.php
$middleware->redirectGuestsTo(function(Request $request) {
+ if (request()->routeIs('admin.*')) {
+ return $request->expectsJson() ? null : route('admin.login');
+ }
if (request()->routeIs('user.*')) {
return $request->expectsJson() ? null : route('user.login');
}
return $request->expectsJson() ? null : route('auth');
});
ルーティング
- routes/web.php
+use App\Http\Controllers\AdminLoginController;
/*** 略 ***/
+// 管理ログイン画面
+Route::get('/admin-login', [AdminLoginController::class, 'create'])->name('admin.login');
+// 管理ログイン
+Route::post('/admin-login', [AdminLoginController::class, 'store'])->name('admin.login.store');
+// 管理ログアウト
+Route::delete('/admin-login', [AdminLoginController::class, 'destroy'])->name('admin.login.destroy');
+
+// 管理ログイン後のみアクセス可
+Route::middleware('auth:admin')->group(function () {
+ Route::get('/admin-top', function () {
+ return view('admin.top');
+ })->name('admin.top');
+});
ビュー
ログインView
php artisan make:view admin/login
- resources/views/admin/login.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>管理</title>
<style>
html,
body {
height: 100%;
}
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
li {
list-style: none;
}
form {
text-align: right;
}
.alert {
width: 300px;
padding: 8px;
}
.alert ul {
padding: 0;
}
.alert ul li {
color: red;
text-align: left;
overflow-wrap: break-word;
}
</style>
</head>
<body>
<main>
<h2>管理ログイン</h2>
<form method="POST" action="{{ route('admin.login.store') }}">
@csrf
<div>
<label for="email">E-main: </label>
<input type="text" id="email" name="email" required>
</div>
<div>
<label for="password">Password: </label>
<input type="password" id="password" name="password" required>
</div>
<div class="alert">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
<button type="submit">ログイン</button>
</form>
</main>
</body>
</html>
トップView
php artisan make:view admin/top
- resources/views/admin/top.blade.php
認証(guards, providers)設定
- config/auth.php
'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,
+ ],
],
セッション管理
ユーザーでログインした場合と管理でログインした場合のID保存領域を分ける
- app/Session/AppDatabaseSessionHandler.php
sessionsテーブルへの保存カラムを分ける
<?php
namespace App\Session;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Session\DatabaseSessionHandler;
class AppDatabaseSessionHandler extends DatabaseSessionHandler
{
/**
* Add the user information to the session payload.
*
* @param array $payload
* @return $this
*/
protected function addUserInformation(&$payload)
{
if ($this->container->bound(Guard::class)) {
if (request()->routeIs('admin.*')) {
$payload['admin_id'] = $this->userId();
} else {
$payload['user_id'] = $this->userId();
}
}
return $this;
}
}
プロバイダーにセッション管理を登録
ルートによってセッション名を変えることでそれぞれのセッションを扱えるようにする
- app/Providers/AppServiceProvider.php
+use App\Session\AppDatabaseSessionHandler;
+use Illuminate\Support\Facades\Session;
/*** 略 ***/
public function boot(): void
{
//
+ Session::extend('database', function ($app) {
+ $session_cookie = config('session.cookie');
+ if (request()->routeIs('admin.*')) {
+ config(['session.cookie' => $session_cookie . '_admin']);
+ } else {
+ config(['session.cookie' => $session_cookie . '_user']);
+ }
+
+ $connection_name = $this->app->config->get('session.connection');
+ $connection = $app->app->db->connection($connection_name);
+ $table = $this->app->config->get('session.table');
+ $lifetime = $this->app->config->get('session.lifetime');
+
+ return new AppDatabaseSessionHandler($connection, $table, $lifetime, $this->app);
+ });
}