[Laravel][マルチ認証] フロントは会員を、管理画面は管理者というように別々に認証を行う
Laravelは認証の仕組みが組み込まれています。
ただ、タイトルの通り
- フロントでは会員情報のID、パスワードで認証
- 管理画面では管理者情報のID、パスワードで認証
というように別々に認証を行いたいことがよくあります。
公式のドキュメントには詳しく掲載されていなかったのですが、Laravelではこういった認証も実装できる柔軟な仕組みを提供しています。
マルチ認証と呼ばれている機能です。
今回はこれを取り扱います。
マルチ認証の説明は下記が非常にわかりやすいです。
このページでは1つのログインフォームで3つのモデルの認証を行う方法が紹介されています。
【Laravel Jetstream】複数モデルでログインできるようにする(Multi Auth) – console dot log
仕様と実装
フロント
- 会員情報のメールアドレス、パスワードで認証
- 会員として認証されていない場合は、ページの一部が表示されない
- モデル: Member
- ルーティング: 「/」にアクセスするとフロントの認証を試みる。その後IndexControllerを呼び出す。
- フロント専用のログインフォームを設置
管理画面
- 管理者情報のユーザーID、パスワードで認証
- 管理者として認証されていない場合は、ログインフォームに強制リダイレクト
- モデル: Administrator
- ルーティング: 「/admin/」配下にアクセスすると管理画面の認証を試みる。その後Admin\IndexControllerを呼び出す
- 管理画面専用のログインフォームを設置
ルーティング設定
- フロント: トップページ
- フロント: ログイン(ログインフォーム表示)
- フロント: ログアウト(ログインフォームにリダイレクト)
- 管理画面: 管理画面トップ
- 管理画面: ログイン(ログインフォーム表示)
- 管理画面: ログアウト(ログインフォームにリダイレクト)
が動くように実装していきます。
まずはルーティングを設定します。
// 管理画面(admin/配下に置くことを想定しています。groupメソッドでまとめると便利です。)
use App\Http\Controllers\Admin;
Route::prefix('admin')->group(function () {
Route::get('login', [Admin\LoginController::class, 'index'])->name('admin.login.index');
Route::post('login', [Admin\LoginController::class, 'login'])->name('admin.login.login');
Route::get('logout', [Admin\LoginController::class, 'logout'])->name('admin.login.logout');
Route::get('/',[Admin\IndexController::class, 'index'])->name('admin.index');
});
// フロント
use App\Http\Controllers;
Route::get('login', [Controllers\LoginController::class, 'index'])->name('login.index');
Route::post('login', [Controllers\LoginController::class, 'login'])->name('login.login');
Route::get('logout', [Controllers\LoginController::class, 'logout'])->name('login.logout');
Route::get('/', [Controllers\IndexController::class, 'index'])->name('index');
管理画面はadmin/配下に置くことを想定しています。
こんなときははprefix()メソッドとgroupメソッドで接頭辞となる「admin」をくくりだしておくと便利です。
うまくgroupメソッドでまとめることで記述量が減らせるだけではなく、今回のような要認証なのに設定が漏れたという事故も防げます。
コンフィグ設定(config/auth.php)
config/auth.phpを修正して設定します。
認証の種類だけ定義を追記していきます。
今回は管理画面用の定義とフロント用の定義を書き加えていきます。
このファイルではガードとプロバイダを定義しています。(補足参照)
ガードとプロバイダは概念が難しいのでひとまずは認証の種類と覚えておけばよいでしょう。
// ファイルの一部のみ抜粋
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// 追加ここから
'administrators' => [
'driver' => 'session',
'provider' => 'administrators',
],
'members' => [
'driver' => 'session',
'provider' => 'members',
],
// 追加ここまで
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// 追加ここから
'administrators' => [
'driver' => 'eloquent',
'model' => App\Models\Administrator::class,
],
'members' => [
'driver' => 'eloquent',
'model' => App\Models\Member::class,
],
// 追加ここまで
],
管理画面用とフロント用に認証を定義しています。
ここでも認証に使うモデルも設定します。
モデルとマイグレーションの作成
Laravelはインストール時点で認証用のモデル(app/Models/User.php)とマイグレーションファイル(database/migrations/〜_create_users_table.php)が作成されています。
それぞれコピーしてマルチ認証用のモデルとマイグレーションファイルを作成します。
- app/Models/User.phpというファイルをコピーしてAdministrator.php、Member.phpという名前のファイルを作成しておきます。
- database/migrations/〜_create_users_table.phpというファイルをコピーして〜_create_administrators_table.php、〜_create_members_table.phpという名前のファイルを作成しておきます。
管理画面
- app/Models/User.phpというファイルをコピーしてAdministrator.phpという名前のファイルを作成しておきます。
- database/migrations/〜_create_users_table.phpというファイルをコピーして〜_create_administrators_table.phpという名前のファイルを作成しておきます。
モデル作成
コピーしたファイルを調整して管理画面用のモデルを作り込んでいきます。
[app/Models/Administrator.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 Laravel\Sanctum\HasApiTokens;
class Administrator extends Authenticatable // User -> Administratorに変更
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'name',
'userid', // email -> useridに変更
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
マイグレーション作成
同様にマイグレーションファイルも修正していきます。
[database/migrations/〜_create_administrators_table.php]
<?php
// 「user」を「administrator」に書き換えていきます。
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAdministratorsTable extends Migration //
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('administrators', function (Blueprint $table) {
$table->id();
$table->string('name')->nullable();
$table->string('userid')->unique(); // email -> useridに変更
$table->timestamp('userid_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->text('extra_administrators_column1')->nullable()->comment('追加カラム1'); // 任意のフィールドを追加しても問題ありません。membersテーブルと構造が違ってもOKです。
$table->text('extra_administrators_column2')->nullable()->comment('追加カラム2');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('administrators');
}
}
フロント
- app/Models/User.phpというファイルをコピーしてMember.phpという名前のファイルを作成しておきます。
- database/migrations/〜_create_users_table.phpというファイルをコピーして〜_create_members_table.phpという名前のファイルを作成しておきます。
モデル作成
コピーしたファイルを調整して管理画面用のモデルを作り込んでいきます。
[app/Models/Member.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 Laravel\Sanctum\HasApiTokens;
class Member extends Authenticatable // User -> Memberに変更
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
マイグレーション作成
同様にマイグレーションファイルも修正していきます。
[database/migrations/〜_create_members_table.php]
<?php
// 「user」を「member」に書き換えていきます。
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateMembersTable extends Migration //
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('members', function (Blueprint $table) {
$table->id();
$table->string('name')->nullable();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->text('extra_members_column1')->nullable()->comment('追加カラム1'); // 任意のフィールドを追加しても問題ありません。administratorssテーブルと構造が違ってもOKです。
$table->text('extra_members_column2')->nullable()->comment('追加カラム2');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('administrators');
}
}
別の作成方法: artisanコマンドで雛形生成
モデルとマイグレーションファイルを作るのが目的なのでartisanコマンドで雛形を生成してから作り込んでいっても問題ありません。
フロントの認証に使うMemberモデルと、管理画面の認証に使うAdministratorモデルをの雛形を生成します。
両方とも-mオプションを付けることでマイグレーションファイルも生成してしまいます。
php artisan make:model Member -m
php artisan make:model Administrator -m
雛形を元に認証用のモデルとして機能するように作り込んでいきます。
どちらにしても、User.phpをコピーするのが早いですが、特に重要な変更はModelの代わりに「Illuminate\Foundation\Auth\User as Authenticatable」を継承するようにすることです。
// app/Models/Administrator.php※一部
use Illuminate\Foundation\Auth\User as Authenticatable; // 追記
// class Administrator extends Model
class Administrator extends Authenticatable // 変更
テストデータ作成
database/seeders/DatabaseSeeder.php
- 管理者のレコード作成
- 会員のレコード作成
を実行するクラスを呼び出しています。
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
// \App\Models\User::factory(10)->create();
$this->call([
AdministratorSeeder::class,
MemberSeeder::class,
]);
}
}
database/seeders/AdministratorSeeder.php
- ユーザーID: admin001
- パスワード: pass0001
みたいなデータを複数作成しておきます。
<?php
namespace Database\Seeders;
use App\Models\Administrator;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class AdministratorSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Administrator::truncate();
$administrators_data = [];
for ($i = 1; $i <= 10; $i++) {
$administrators_data[] = [
'userid' => sprintf('admin%03d', $i),
'password' => sprintf('pass%04d', $i),
];
}
foreach($administrators_data as $data) {
$administrator = new Administrator();
$administrator->userid = $data['userid'];
$administrator->password = Hash::make($data['password']);
$administrator->save();
}
}
}
database/seeders/MemberSeeder.php
- ユーザーID: member01
- パスワード: pass0001
みたいなデータを複数作成しておきます。
<?php
namespace Database\Seeders;
use App\Models\Member;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class MemberSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Member::truncate();
$member_data = [];
for ($i = 1; $i <= 10; $i++) {
$member_data[] = [
'email' => sprintf('member%02d@example.com', $i),
'password' => sprintf('pass%04d', $i),
];
}
foreach($member_data as $data) {
$member = new Member();
$member->email = $data['email'];
$member->password = Hash::make($data['password']);
$member->save();
}
}
}
シーダーファイルを作ったら、artisanコマンドでテーブルを作ります。
php artisan db:seed
コントローラー・ビューの作成(管理画面)
ログインフォーム
コントローラー(app/Http/Controllers/Admin/LoginController.php)
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function index()
{
return view('admin.login.index');
}
public function login(Request $request)
{
$credentials = $request->only(['userid', 'password']);
if (Auth::guard('administrators')->attempt($credentials)) {
// ログインしたら管理画面トップにリダイレクト
return redirect()->route('admin.index')->with([
'login_msg' => 'ログインしました。',
]);
}
return back()->withErrors([
'login' => ['ログインに失敗しました'],
]);
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
// ログアウトしたらログインフォームにリダイレクト
return redirect()->route('admin.login.index')->with([
'logout_msg' => 'ログアウトしました',
]);
}
}
メソッドは3つあります。
index(): ログインフォームの表示(GET)
login(): ログインを試みる(POST)
logout(): ログアウトを行ってログインフォームにリダイレクト(GET)
管理画面はメールアドレスではなく、ユーザーIDで認証する想定なので、
$credentials = $request->only(['userid', 'password']);
という記述になっています。(emailではない。)
routeメソッドの名前は任意ですが、後でルーティング設定のときに同じ名前を付ける必要があります。
ビュー(admin/login/index.blade.php)
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1>管理者ログイン</h1>
@error('login')
<div class="alert alert-danger">
⚠ {{ $message }}
</div>
@enderror
<form method="POST" action="/admin/login">
@csrf
<label class="mt-3">ユーザーID</label>
<input type="text" name="userid" class="form-control">
<label class="mt-3">パスワード</label>
<input class="form-control" type="password" name="password">
<button class="btn btn-primary mt-5" type="submit">ログイン</button>
</form>
</div>
</body>
</html>
ただのログインフォームです。
@errorディレクティブの中身は、コントローラーで記述したwithErrorsメソッドの設定が反映されます。
return back()->withErrors([
'login' => ['ログインに失敗しました'], // この内容が$messageに展開
]);
ログイン後リダイレクトされる管理画面トップ
コントローラー(app/Http/Controllers/Admin/IndexController.php)
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class IndexController extends Controller
{
public function index()
{
return view('admin.index');
}
}
単にビューファイルを返すだけのメソッドです。
ビュー(admin/index.blade.php)
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1>管理画面トップ</h1>
@if (session('login_msg'))
<div class="alert alert-success">
{{ session('login_msg') }}
</div>
@endif
@if (Auth::guard('administrators')->check())
<div>ユーザーID {{ Auth::guard('administrators')->user()->userid }}でログイン中</div>
@endif
<ul>
<li>ログイン状態: {{ Auth::check() }}</li>
<li>管理者(Administrator)ログイン状態: {{ Auth::guard('administrators')->check() }}</li>
<li>会員(members) ログイン状態: {{ Auth::guard('members')->check() }}</li>
</ul>
<div>
<a href="/admin/logout">ログアウト</a>
</div>
</div>
</body>
</html>
Auth::check()で認証済かどうかを判定できます。
さらに引数にガードの種類を指定することで、特定のユーザーで認証認証済かどうかを判定できます。(今回は管理者の認証状態と会員の認証状態を別々に判別できるということです。当然別々に判別できないと困りますが。。)
コントローラー・ビューの作成(フロント)
ログインフォーム
コントローラー(app/Http/Controllers/LoginController.php)
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function index()
{
return view('login.index');
}
public function login(Request $request)
{
$credentials = $request->only(['email', 'password']);
$guard = $request->guard;
if (Auth::guard('members')->attempt($credentials)) {
// ログインしたらトップページにリダイレクト
return redirect()->route('index')->with([
'login_msg' => 'ログインしました。',
]);
}
return back()->withErrors([
'login' => ['ログインに失敗しました'],
]);
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
// ログアウトしたらログインフォームにリダイレクト
return redirect('login.index')->with([
'auth' => ['ログアウトしました'],
]);
}
}
メソッドの構成や処理は管理画面と同様です。
ビュー(login/index.blade.php)
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1>会員ログイン</h1>
@error('login')
<div class="alert alert-danger">
⚠ {{ $message }}
</div>
@enderror
<form method="POST" action="/login">
@csrf
<label class="mt-3">ユーザーID</label>
<input type="text" name="email" class="form-control">
<label class="mt-3">パスワード</label>
<input class="form-control" type="password" name="password">
<button class="btn btn-primary mt-5" type="submit">ログイン</button>
</form>
</div>
</body>
</html>
ログイン後リダイレクトされるトップページ
コントローラー
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class IndexController extends Controller
{
public function index()
{
return view('index');
}
}
こちらもビューを返すだけの記述です。
ビュー(index.blade.php)
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1>トップページ</h1>
@if (session('login_msg'))
<div class="alert alert-success">
{{ session('login_msg') }}
</div>
@endif
@if (Auth::guard('members')->check())
<div>ユーザーID {{ Auth::guard('members')->user()->userid }}でログイン中</div>
@else
<div>ログインしていません</div>
@endif
<ul>
<li>ログイン状態: {{ Auth::check() }}</li>
<li>管理者(Administrator)ログイン状態: {{ Auth::guard('administrators')->check() }}</li>
<li>会員(members) ログイン状態: {{ Auth::guard('members')->check() }}</li>
</ul>
<div>
<a href="/admin/logout">ログアウト</a>
</div>
</div>
</body>
</html>
middleware設定をしていないので会員認証していなくてもこのページは見えるようになっています。
認証しているかしていないかで表示されるメッセージが変わります。
下記の箇所です。
@if (Auth::guard('members')->check())
<div>ユーザーID {{ Auth::guard('members')->user()->userid }}でログイン中</div>
@else
<div>ログインしていません</div>
@endif
ミドルウェアの設定
最後にミドルウェアの設定をします。
ここまでの設定でログインの流れは実装できました。
しかし管理画面で認証していない場合はログインフォームにリダイレクトさせるという仕様を実装していません。
Laravelはミドルウェアという仕組みを提供しています。
これはルーティングに設定することで前処理や後処理を追加できるものです。
今回は管理画面のアクセス前にauthミドルウェアを実行させることで認証していない場合にログインフォームにリダイレクトさせます。
今回、フロントは会員として認証していなくてもページ自体は閲覧できる想定です。
そのためmiddleware設定は行っていません。(記述するとリダイレクトされてしまう。)
routes/web.phpの調整
// routes/web.php(一部)
// 管理画面(admin/配下に置くことを想定しています。groupメソッドでまとめると便利です。)
use App\Http\Controllers\Admin;
Route::prefix('admin')->group(function () {
Route::get('login', [Admin\LoginController::class, 'index'])->name('admin.login.index');
Route::post('login', [Admin\LoginController::class, 'login'])->name('admin.login.login');
Route::get('logout', [Admin\LoginController::class, 'logout'])->name('admin.login.logout');
});
// 管理者(administratorsテーブル)未認証の場合にログインフォームに強制リダイレクトさせるミドルウェアを設定する。
Route::prefix('admin')->middleware('auth:administrators')->group(function () {
Route::get('/',[Admin\IndexController::class, 'index'])->name('admin.index');
});
app/Heep/Middleware/Authenticate.phpの調整
redirectToというメソッドで強制リダイレクトを実現しています。
web.phpで設定したルートの名前に合わせてメソッドの戻り値を調整します。
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Support\Str;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request)
{
return route('admin.login.index'); // 管理画面トップのルート名を記述することで、管理画面ログインフォームにリダイレクト
}
}
補足
ミドルウェアは自作可能です。
今回は管理画面のみ強制リダイレクトなのでもとからあるミドルウェアを改変しました。
例えば管理画面と会員専用ページにそれぞれ強制リダイレクトみたいな仕様が求められたときはどうすればよいでしょう?
- URLを解析してリダイレクト先を振り分ける
- 会員認証チェック、管理者認証チェックのミドルウェアを用意する
のいずれかで対応できます。
2. 会員認証チェック、管理者認証チェックのミドルウェアを用意する
app/Heep/Middleware/MemberAuthenticate.phpというファイルを作って中身を調整します。
app/Http/Kernel.phpにミドルウェアを登録します。
// 一部
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'auth.members' => \App\Http\Middleware\Authenticate::class, // 追記
];
routes/web.phpでmiddlewareメソッドを記述する。
このとき middleware('auth:members')ではなくmiddleware('auth.members:members')となります。
Discussion
こちらを参考に管理者ログイン機能を実装させていただきました。
この後、ログインに3回失敗すると二分間ログインできないようなブロック機能を管理者ログインに実装したいと考えています。しかし、自分で調べてもわからなかったため、ソースコードを教えていただけないでしょうか。
「app/Heep/Middleware/MemberAuthenticate.phpというファイルを作って中身を調整します」という部分なのですが具体的にはapp/Heep/Middleware/Authenticate.phpのファイルのどの部分を変更すれば良いのでしょうか?
( return route('admin.login.index');の部分のadmin.login.indexを表示させたいテンプレートに変更するという点は分かります。 )