【Laravel7でユーザー認証_9】マルチ認証機能を使って管理者を作成する

#はじめに
Laravelのユーザー認証機能に、管理者権限を追加する（マルチ認証機能を使う）手順をまとめます。

# 環境

XAMPP環境でLaravelが使えるように設定してあります。

- Windows10 Pro 64bit
- PHP 7.3.18
- Laravel 7.12.0
- MariaDB 10.1.32

また、Laravelプロジェクトは以下の手順で作業を進めており、ユーザーは自身で情報を登録・変更・削除ができるようになっています。

- [【Laravel7でユーザー認証_1】基本のき](https://qiita.com/crosawassant/items/bf3982666b6333bcb8c8)
- [【Laravel7でユーザー認証_2】ユーザー認証を日本語化](https://qiita.com/crosawassant/items/d1f4c5c93f30afa9eb65)
- [【Laravel7でユーザー認証_3】ユーザー認証をメールアドレスからユーザー名に変更する](https://qiita.com/crosawassant/items/2464db8a0fe1c603401a)
- [【Laravel7でユーザー認証_4】パスワード変更フォームを作成する](https://qiita.com/crosawassant/items/04242c8d7a12346dcc20)
- [【Laravel7でユーザー認証_5】ユーザーを倫理削除（SoftDelete）する](https://qiita.com/crosawassant/items/a6e3e9b1aedf566b2d27)
- [【Laravel7でユーザー認証_6】ユーザーの情報を表示・変更する設定画面を作成する](https://qiita.com/crosawassant/items/d8b434f0bc98455165b4)
- [【Laravel7でユーザー認証_7】会員登録時にメール認証を行う](https://qiita.com/crosawassant/items/eed947b1e516d864fc6c)
- [【Laravel7でユーザー認証_8】メールアドレス変更時にメール認証を行う](https://qiita.com/crosawassant/items/018b29ab770c0a373bc9)

#実装手順

##Adminモデルとadminsテーブルの作成

以下のコマンドで、Adminモデルとマイグレーションファイルを作成します。

```
php artisan make:model Admin -m
```

Adminモデルは、Userモデルの内容をコピーして使います。

```php:app/Admin.php
<?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:database/migrations/日付_create_admins_table.php
<?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が使うガードとプロバイダを設定します。

```diff:config/auth.php
      '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を定義します。

```diff:app/Providers/RouteServiceProvider.php
      /**
       * The path to the "home" route for your application.
       *
       * @var string
       */
      public const HOME = '/home';
+     public const ADMIN_HOME = '/admin/home';
```

##リダイレクト処理

###未ログイン時のリダイレクト

管理者としてログインしていない閲覧者が管理者権限が必要なページにアクセスした場合、ログイン画面にリダイレクトさせる設定をします。

```diff:app/Http/Middleware/Authenticate.php
+ 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にリダイレクトさせるよう設定します。

```diff:app/Http/Middleware/RedirectIfAuthenticated.php
      /**
       * 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`

```diff: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`



```diff: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.` を指定

```diff:routes/web.php
+ 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の構成は、以下のようにしました。
<pre>
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をコピー
</pre>

▼@guest で判定していた箇所は、 @if (Auth::guard('admin')->check()) で、管理者かどうかのチェックをするように変更しました。

```php:resources/views/layouts/admin/app.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">

    <!-- 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` を読み込むようにします。

```php:resources/views/admin/home.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となるようにルートを指定します。

```php:resources/views/admin/login.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">{{ __('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という翻訳文字列を使ったので、翻訳ファイルに追加します。

```diff:resources/lang/ja.json
+     "Admin": "管理者"
  }
```


##管理者情報の登録

`http://127.0.0.1:8000/admin/login` にアクセスすると、管理者ログインフォームが表示されるまではできあがりました。
ですが、管理者の登録画面を作っていないので、このままだとログインも登録もできない状態です。
まずはログインができるかどうかを試したいので、データベースに管理者のデータを直接入れ込んでしまって確認します。

SQLで直接データを入れてしまってもいいのですが、せっかくなのでLaravelのシーダー機能を使ってデータを入れてみます。

###シーダーファイルの作成

`AdminsTableSeeder` というシーダーファイルを作成します。

```
php artisan make:seeder AdminsTableSeeder
```

adminsテーブルに入っていてほしいデータを指定します。

|項目|カラム名|値|
|:--|:--|:--|
|氏名|name|管理者|
|ユーザー名|username|admin|
|メールアドレス|email|admin@example.com|
|パスワード|password|secret|

```database/seeds/AdminsTableSeeder.php
<?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') 
               ]
          ]);
    }
}
```

```diff:database/seeds/DatabaseSeeder.php
      public function run()
      {
-         //
+         $this->call(AdminsTableSeeder::class);
      }
```

シーダーを実行して、データが入ったかどうか確認します。

```
php artisan db:seed
```

##動作確認

設定した情報でログインができるかどうか確認します。

▼私はこんなエラーと戦いました。
<dl><dt>フォーム表示→ Auth guard [admin] is not defined.</dt><dd>`config/app.php` を変更した後、`php artisan config:cache` をしていませんでした。
</dd>
<dt>ログイン→ Class 'App\Http\Controllers\Admin\Auth\Auth' not found</dt>
<dd>`app/Http/Controllers/Admin/Auth/LoginController.php` に、`use Illuminate\Support\Facades\Auth;` が抜けていました。</dd>
<dt>ログイン→ Class 'App\Http\Controllers\Admin\Controller' not found</dt>
<dd>`app/Http/Controllers/Admin/HomeController.php` に、`use App\Http\Controllers\Controller;` が抜けていました。</dd>
<dt>未ログイン状態で /admin/home にアクセス→ `Class 'App\Http\Middleware\Route' not found`</dt>
<dd>`app/Http/Controllers/Middleware/Authenticate.php` に `use Illuminate\Support\Facades\Route;` が抜けていました。
リダイレクト分岐判定のRouteの前にバックスラッシュを置いてグローバルクラスにすれば（ `if (\Route::is('admin.*')) {` ）useを使わなくてもよいようです。</dd>
</dl>


#おわりに

管理者のログイン、ホームの表示ができるようになりました。
機能としてはまだ不十分なので、次回は、管理者のパスワード変更やリセットができるように修正したいと思います。

#参考サイト
- [認証 7.x Laravel](https://readouble.com/laravel/7.x/ja/authentication.html)
- [Laravel 7.xでマルチ認証機能を作成してみた - arms inc. Engineers&#39; Blog](https://tech.arms-soft.co.jp/entry/2020/04/22/090000)
- [Laravel 5.4/5.5 で Multi-Auth を実装する - Qiita](https://qiita.com/s_of_p/items/3c066f56123cb05a24f1)
- [Auth機能にシーダーを実行＆各画面の確認（Laravel6） - Laravel学習帳](https://laraweb.net/tutorial/6857/)