1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravel 11 web画面を使用したマルチ認証導入手順

Posted at

やること

  • ユーザーのログイン認証を作成

  • 管理のログイン認証を作成

  • ログイン状態を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);
+        });
    }

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?