はじめに
Laravelのユーザー認証機能に、管理者権限を追加する(マルチ認証機能を使う)手順をまとめます。
環境
XAMPP環境でLaravelが使えるように設定してあります。
- Windows10 Pro 64bit
- PHP 7.3.18
- Laravel 7.12.0
- MariaDB 10.1.32
また、Laravelプロジェクトは以下の手順で作業を進めており、ユーザーは自身で情報を登録・変更・削除ができるようになっています。
- 【Laravel7でユーザー認証_1】基本のき
- 【Laravel7でユーザー認証_2】ユーザー認証を日本語化
- 【Laravel7でユーザー認証_3】ユーザー認証をメールアドレスからユーザー名に変更する
- 【Laravel7でユーザー認証_4】パスワード変更フォームを作成する
- 【Laravel7でユーザー認証_5】ユーザーを倫理削除(SoftDelete)する
- 【Laravel7でユーザー認証_6】ユーザーの情報を表示・変更する設定画面を作成する
- 【Laravel7でユーザー認証_7】会員登録時にメール認証を行う
- 【Laravel7でユーザー認証_8】メールアドレス変更時にメール認証を行う
実装手順
Adminモデルとadminsテーブルの作成
以下のコマンドで、Adminモデルとマイグレーションファイルを作成します。
php artisan make:model Admin -m
Adminモデルは、Userモデルの内容をコピーして使います。
<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Admin extends Authenticatable implements MustVerifyEmail
{
use Notifiable;
use SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'username', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
マイグレーションファイルも、現在のusersテーブルの仕様に合わせます。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAdminsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('admins', function (Blueprint $table) {
$table->id();
$table->string('username', 32);
$table->string('name');
$table->string('email');
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
$table->unique(['email', 'deleted_at'], 'admins_email_deleted_at_unique');
$table->unique(['username', 'deleted_at'], 'admins_username_deleted_at_unique');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('admins');
}
}
マイグレーションを実行すると、usersと同じ構造のadminsテーブルが出来上がります。
php artisan migrate
管理者用のガードとプロバイダを追加
認証の設定ファイル config/auth.php
に、adminが使うガードとプロバイダを設定します。
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
+
+ 'admin' => [
+ 'driver' => 'session',
+ 'provider' => 'admins',
+ ],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
+
+ 'admins' => [
+ 'driver' => 'eloquent',
+ 'model' => App\Admin::class,
+ ],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
configファイル変更後は、キャッシュをクリアしておきます。
php artisan config:cache
ADMIN_HOME の定義
管理者用のhomeとして、ADMIN_HOMEを定義します。
/**
* The path to the "home" route for your application.
*
* @var string
*/
public const HOME = '/home';
+ public const ADMIN_HOME = '/admin/home';
リダイレクト処理
未ログイン時のリダイレクト
管理者としてログインしていない閲覧者が管理者権限が必要なページにアクセスした場合、ログイン画面にリダイレクトさせる設定をします。
+ use Illuminate\Support\Facades\Route;
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)
{
if (! $request->expectsJson()) {
- return route('login');
+ if (Route::is('admin.*')) {
+ return route('admin.login');
+ } else {
+ return route('login');
+ }
}
}
管理者ログイン直後のリダイレクト
管理者としてログインした際、ADMIN_HOMEにリダイレクトさせるよう設定します。
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
- return redirect(RouteServiceProvider::HOME);
+ if ($guard === 'user') {
+ return redirect(RouteServiceProvider::HOME);
+ } elseif ( $guard === 'admin') {
+ return redirect(RouteServiceProvider::ADMIN_HOME);
+ }
}
return $next($request);
}
コントローラ作成
ユーザー用のコントローラをコピーして、管理者用の HomeController
と LoginController
を作成します。
コピー元:app/Http/Controllers/HomeController.php
コピー先:app/Http/Controllers/Admin/HomeController.php
<?php
- namespace App\Http\Controllers;
+ namespace App\Http\Controllers\Admin;
use Illuminate\Http\Request;
class HomeController.php extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
- $this->middleware('auth');
+ $this->middleware('auth:admin');
}
/**
* Show the application dashboard.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{
- return view('home');
+ return view('admin.home');
}
}
コピー元:app/Http/Controllers/Auth/LoginController.php
コピー先:app/Http/Controllers/Admin/Auth/LoginController.php
<?php
- namespace App\Http\Controllers\Auth;
+ namespace App\Http\Controllers\Admin\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
+ use Illuminate\Http\Request;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
- protected $redirectTo = RouteServiceProvider::HOME;
+ protected $redirectTo = RouteServiceProvider::ADMIN_HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
- $this->middleware('guest')->except('logout');
+ $this->middleware('guest:admin')->except('logout');
}
+
+ protected function guard()
+ {
+ return Auth::guard('admin');
+ }
+
+ //ログインフォームのview指定
+ public function showLoginForm()
+ {
+ return view('admin.auth.login');
+ }
+
+ //ログアウトの処理
+ public function logout(Request $request)
+ {
+ Auth::guard('admin')->logout();
+ return $this->loggedOut($request);
+ }
+
+ //ログアウト時のリダイレクト先
+ public function loggedOut(Request $request)
+ {
+ return redirect(route('admin.login'));
+ }
public function username()
{
return 'username';
}
}
ルーティングの設定
-
App\Http\Controllers\Admin
名前空間下のコントローラを読み込ませたいので、namespace
メソッドでAdmin
を指定 - 管理者用のURIは、
admin/
でまとめたいので、prefix
メソッドでadmin
を指定 - ルート名もまとめてadmin.というプリフィックスをつけたいので、
name
メソッドでadmin.
を指定
+ Route::namespace('Admin')->prefix('admin')->name('admin.')->group(function() {
+ Route::get('home', 'HomeController@index')->name('home');
+ Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
+ Route::post('login', 'Auth\LoginController@login');
+ Route::post('logout', 'Auth\LoginController@logout')->name('logout');
+ }
管理者用のviewを作成
管理者用とユーザー用の画面がわかりやすいように、管理者はダークモードにしたいと思います。
viewの構成は、以下のようにしました。
views/
├ admin/
│ ├ auth/
│ │ └ login.blade.php #resources/views/auth/login.blade.phpをコピー
│ └ home.blade.php #resources/views/home.blade.phpをコピー
│
└ layouts/
└ admin/
└ app.blade.php #resources/views/layouts/app.blade.phpをコピー
▼@guest で判定していた箇所は、 @if (Auth::guard('admin')->check()) で、管理者かどうかのチェックをするように変更しました。
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-md navbar-dark bg-dark shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url('/') }}">
{{ config('app.name', 'Laravel') }}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<!-- Left Side Of Navbar -->
<ul class="navbar-nav mr-auto">
</ul>
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Authentication Links -->
@if (Auth::guard('admin')->check())
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{ Auth::user()->name }} <span class="caret"></span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ route('admin.logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }}
</a>
<form id="logout-form" action="{{ route('admin.logout') }}" method="POST" style="display: none;">
@csrf
</form>
</div>
</li>
@else
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.login') }}">{{ __('Login') }}</a>
</li>
@endif
</ul>
</div>
</div>
</nav>
<main class="py-4">
@yield('content')
</main>
</div>
</body>
</html>
▼ヘッダ部分について、作成した layout/admin/app.blade.php
を読み込むようにします。
@extends('layouts.admin.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">Dashboard</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
You are logged in!
</div>
</div>
</div>
</div>
</div>
@endsection
▼ヘッダ部分について、作成した layout/admin/app.blade.php
を読み込むようにして、formのaction先もadminのloginとなるようにルートを指定します。
@extends('layouts.admin.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Admin') }}{{ __('Login') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.login') }}">
@csrf
<div class="form-group row">
<label for="username" class="col-md-4 col-form-label text-md-right">{{ __('UserName') }}</label>
<div class="col-md-6">
<input id="username" type="username" class="form-control @error('username') is-invalid @enderror" name="username" value="{{ old('username') }}" required autocomplete="username" autofocus>
@error('username')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<div class="col-md-6 offset-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
<label class="form-check-label" for="remember">
{{ __('Remember Me') }}
</label>
</div>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Admin') }}{{ __('Login') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
翻訳ファイルに追記
view内で新たにAdminという翻訳文字列を使ったので、翻訳ファイルに追加します。
+ "Admin": "管理者"
}
管理者情報の登録
http://127.0.0.1:8000/admin/login
にアクセスすると、管理者ログインフォームが表示されるまではできあがりました。
ですが、管理者の登録画面を作っていないので、このままだとログインも登録もできない状態です。
まずはログインができるかどうかを試したいので、データベースに管理者のデータを直接入れ込んでしまって確認します。
SQLで直接データを入れてしまってもいいのですが、せっかくなのでLaravelのシーダー機能を使ってデータを入れてみます。
シーダーファイルの作成
AdminsTableSeeder
というシーダーファイルを作成します。
php artisan make:seeder AdminsTableSeeder
adminsテーブルに入っていてほしいデータを指定します。
項目 | カラム名 | 値 |
---|---|---|
氏名 | name | 管理者 |
ユーザー名 | username | admin |
メールアドレス | admin@example.com | |
パスワード | password | secret |
<?php
use Illuminate\Database\Seeder;
class AdminsTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
DB::table('admins')->delete();
DB::table('admins')->insert([
[
'name' => '管理者',
'username' => 'admin',
'email' => 'admin@example.com',
'email_verified_at' => date('Y-m-d H:i:s'),
'password' => Hash::make('secret'),
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
]
]);
}
}
public function run()
{
- //
+ $this->call(AdminsTableSeeder::class);
}
シーダーを実行して、データが入ったかどうか確認します。
php artisan db:seed
動作確認
設定した情報でログインができるかどうか確認します。
▼私はこんなエラーと戦いました。
- フォーム表示→ Auth guard [admin] is not defined.
-
config/app.php
を変更した後、php artisan config:cache
をしていませんでした。
- ログイン→ Class 'App\Http\Controllers\Admin\Auth\Auth' not found
-
app/Http/Controllers/Admin/Auth/LoginController.php
に、use Illuminate\Support\Facades\Auth;
が抜けていました。 - ログイン→ Class 'App\Http\Controllers\Admin\Controller' not found
-
app/Http/Controllers/Admin/HomeController.php
に、use App\Http\Controllers\Controller;
が抜けていました。 - 未ログイン状態で /admin/home にアクセス→
Class 'App\Http\Middleware\Route' not found
-
app/Http/Controllers/Middleware/Authenticate.php
にuse Illuminate\Support\Facades\Route;
が抜けていました。
リダイレクト分岐判定のRouteの前にバックスラッシュを置いてグローバルクラスにすれば(if (\Route::is('admin.*')) {
)useを使わなくてもよいようです。
おわりに
管理者のログイン、ホームの表示ができるようになりました。
機能としてはまだ不十分なので、次回は、管理者のパスワード変更やリセットができるように修正したいと思います。