Laravel5.7でMulti Authを試す。


散々やったMultiAuthだけど、ずいぶんブランクがあるのでおさらい。
[こちら](https://www.willstyle.co.jp/blog/907/)の記事を参考にさせていただきました。が、5.7や私の環境ではそのまま動かないところがあったので一部修正・追記しております。

## やりたいこと

* usersに加え、adminsテーブルを追加し、ユーザー側、管理側で認証を分ける。
* logoutに関しても個別に行えるようにする（とりあえずadminでログアウトしてもuserのSessionは維持）

## 方針

とりあえずmake:authで生成されるusers向けのControllerやViewを応用して管理者用（admin）の認証機能を作る。

## 大まかな流れ

1. laravelのインストールや.envの設定 make:auth
2. adminsテーブルの作成、Adminモデルの生成
3. admin guard（とプロバイダ）の追加
4. Handle.phpに認証エラー時の対応追加
5. routing追加
6. Controllerの追加と編集
7. Viewの追加と編集
8. 動作確認

## 注意（前提知識）

作業中、コードを変更してもうまく反映されないときがあった。その場合、いろいろ[クリア](https://qiita.com/Ping/items/10ada8d069e13d729701)してみる。
特に、今回はmake:authで生成されたファイル（Controller）をコピーして使用したのですが、名前空間を変更しているのにもかかわらず「同じControllerがあるぞ」と怒られることが多かった。なんで？

>composer dump-autoloadしてみると「重複してるぞ」的なメッセージがでる。

## 準備

### Laravelのインストール

composer入ってること前提。とりあえず下記で。

```bash
composer create-project laravel/laravel laravel
```

### .envの設定

詳細は割愛します。
DBへの接続設定をして、migrateが行えるようにしておきます。

### make:authしておく

ひとまずauthに必要な画面・コントローラー等をスキャフォールドで作っておきます。

```bash
php artisan make:auth
```

## テーブルとモデルの作製

管理者情報保存用テーブルとしてadminsを用意します。

```bash
php artisan make:migration create_admins_table
```

>php artisan make:model Admin -m としたほうがいいかも。

生成されたマイグレーションファイルを編集。

```diff:database/migrations/xxxx_create_admins_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateAdminsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->increments('id');
+           $table->string('name');
+           $table->string('email')->unique();
+           $table->string('password');
+           $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('admins');
    }
}

```

migrate実行。

```bash
php artisan migrate
```

Adminモデルを生成。

```bash
php artisan make:model Admin
```

モデルを編集。

認証につかうモデルはAuthenticatableじゃないとだめなので継承します。

```diff:app/Admin
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
+use Illuminate\Foundation\Auth\User as Authenticatable;

+class Admin extends Authenticatable
{
+   protected $fillable = [
+       'name', 'email', 'password',
+   ];

+   protected $hidden = [
+       'password', 'remember_token',
+   ];
}
```

## auth.phpの設定

Guardを追加していきます。
下記ではuserも追加しているが、基本、admin（だけ）を追加すればよい。

```diff:config/auth.php

<?php

return [

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],


    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
        ],
+       'user' => [
+           'driver' => 'session',
+           'provider' => 'users',
+       ],
+       'admin' => [
+           'driver' => 'session',
+           'provider' => 'admins',
+       ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],
+       'admins' => [
+           'driver' => 'eloquent',
+           'model' => App\Admin::class,
+       ],
    ],


    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
        ],
+       'admins' => [
+           'provider' => 'admins',
+           'table' => 'password_resets',
+           'expire' => 60,
+       ],
    ],

];

```

## Handle.phpの編集

認証エラー時（特にJSON利用）の対応をカスタマイズします。

5.5以降書き方が変わったらしい（標準でunauthenticated()の記述がなくなった）。
が、普通にオーバーライドすればよい。

>但し、いくつかuseしてやらないと、エラーがでる。

```diff:app/Exceptions/Handler.php

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

//追加が必要
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+use Illuminate\Auth\AuthenticationException;


class Handler extends ExceptionHandler
{
    protected $dontReport = [
        //
    ];

    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    public function report(Exception $exception)
    {
        parent::report($exception);
    }

    public function render($request, Exception $exception)
    {
        return parent::render($request, $exception);
    }
	
	//オーバーライド
+   protected function unauthenticated($request, AuthenticationException $exception)
+   {
+       if($request->expectsJson()) {
+           return response()->json(['error' => 'Unauthenticated.'], 401);
+       }

+       if(in_array('admin', $exception->guards())){
+           return redirect()->guest('admin/login');
+       }

+       return redirect()->guest(route('login'));
    }
}

```

## ルーティング

とりあえず最低限のルートを設定する。

* adminのホーム
* login/logout関連ページ
* register関連ページ

>パスワードリセット等は随時追加する。

```diff:routes/web.php

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

+Route::group(['prefix' => 'admin'], function(){

//home
+    Route::get('home', 'Admin\HomeController@index')->name('admin.home');
//login logout   
+    Route::get('login', 'Admin\Auth\LoginController@showLoginForm')->name('admin.login');
+    Route::post('login', 'Admin\Auth\LoginController@login')->name('admin.login');
+    Route::post('logout', 'Admin\Auth\LoginController@logout')->name('admin.logout');
//register
+    Route::get('register', 'Admin\Auth\RegisterController@showRegisterForm')->name('admin.register');
+    Route::post('register', 'Admin\Auth\RegisterController@register')->name('admin.register');

+});

Route::get('/home', 'HomeController@index')->name('home');

```

## Controllerの用意

### ファイルのコピー

make:authでuser向けに生成されたものをコピーして利用します。
まず、コピー先としてapp/Http/Controllers/Adminディレクトリを作成します。

#### Admin Homeページ

app/Http/Controllers直下にあるHomeController.phpとController.phpをAdminフォルダ直下にコピーします。

#### 各ページ

app/Http/Controllers/AuthをAdminフォルダにコピーします。
つまり、app/Http/Controllers/Admin/Authとなるようにします。

### ファイルの編集

#### HomeController.php

admin用のhomeページ。要認証（未認証ならadmin.loginへ移動）。

```diff:app/Http/Controllers/Admin/HomeController.php
<?php

+namespace App\Http\Controllers\Admin;

use Illuminate\Http\Request;

class HomeController extends Controller
{
    public function __construct()
    {
+       $this->middleware('auth:admin');
    }

    public function index()
    {
+       return view('admin.home');
    }
}
```

#### Controller.php

継承用のクラス。名前空間のみ変更。

```diff:app/Http/Controllers/Admin/Controller.php
<?php

+namespace App\Http\Controllers\Admin;

use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
```


#### LoginController.php

Login機能の実装。機能の多くはAuthenticatesUsersトレイトに実装されているので、オーバーライドして使う。
formの表示および標準のguardをadminにしている（これにより、ログインに利用される認証参照先がadminとなる）。

なお、トレイトで定義されているlogoutには全てのセッションをリセットする機能が入っているのでその機能をコメントアウトしている（じゃないと、userとadmin両方でログインしている場合、両方ログアウトされる）。ここではadmin用だけlogoutをオーバーライドしているが、必要に応じてuser側もオーバーライドしてもよい。

>session()->invalidate()はセッションがリセットされる（ID再発行される）。

```diff:app/Http/Controllers/Admin/Auth/LoginController.php
<?php

+namespace App\Http\Controllers\Admin\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
+use Illuminate\Http\Request;

class LoginController extends Controller
{

    use AuthenticatesUsers;

+   protected $redirectTo = '/admin/home';

    public function __construct()
    {
+     $this->middleware('guest:admin')->except('logout');
    }

+   public function showLoginForm()
+   {
+       return view('admin.auth.login');
+   }

+   protected function guard()
+   {
+       return \Auth::guard('admin');
+   }

+   public function logout(Request $request)
+   {
+       $this->guard('admin')->logout();
+       // $request->session()->invalidate(); これが全部のSessionを消してしまう
+       return redirect('/');
    }

}
```

#### RegisterController

こちらも基本機能はRegistersUsersトレイトに実装されているので、オーバーライドして利用する。
fromの表示と標準guardの設定をしている。

```diff:app/Http/Controllers/Admin/Auth/RegisterController.php
<?php

+namespace App\Http\Controllers\Admin\Auth;

use App\User;
+use App\Admin; //追加

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;

class RegisterController extends Controller
{

    use RegistersUsers;

+   protected $redirectTo = '/admin/home';

    public function __construct()
    {
+       $this->middleware('guest:admin');
    }

+   public function showRegisterForm()
+   {
+       return view('admin.auth.register');
+   }

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|string|max:255',
+            'email' => 'required|string|email|max:255|unique:admins',
            'password' => 'required|string|min:6|confirmed',
        ]);
    }

    protected function create(array $data)
    {
+       return Admin::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }

+    protected function guard()
+    {
+        return \Auth::guard('admin');
+    }

}
```

## Viewの準備

### ファイルのコピー

#### 共通（レイアウト）ページ

resources/views/layouts/app.blade.phpを複製し、同じ階層にadmin.blade.phpを作成します。

#### Admin Homeページ

resources/views/adminディレクトリを作成します。
resources/views/home.blade.phpをadmin/home.blade.phpとしてコピーします。

#### 各ページ

admin内にresources/views/authをコピーします。
つまり、resources/views/admin/authとなります。

###　共通ページ(admin.blade.php)の編集

管理者用と分かるために背景を変更。login,registerおよびlogoutの飛び先をadmin.*以下に設定。

```diff:resources/views/layout/admin.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="https://fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet" type="text/css">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
+   <style>
+       body{
+           background-color: #aaa;
+       }
+   </style>
</head>
<body>
    <div id="app">
        <nav class="navbar navbar-expand-md navbar-light navbar-laravel">
            <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 -->
                        @guest
                            <li class="nav-item">
+                               <a class="nav-link" href="{{ route('admin.login') }}">{{ __('Login') }}</a>
                            </li>
                            <li class="nav-item">
+                               @if (Route::has('register'))
+                                    <a class="nav-link" href="{{ route('admin.register') }}">{{ __('Register') }}</a>
                                @endif
                            </li>
                        @else
                            <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>
                        @endguest
                    </ul>
                </div>
            </div>
        </nav>

        <main class="py-4">
            @yield('content')
        </main>
    </div>
</body>
</html>

```

## 各ページの編集

### resources/views/admin/home.blade.php

特に機能面で編集は必要ありませんが、テンプレートの変更、それとDashboardをAdmin Dashboardと変更しておいてもいいかもしれません。
詳細割愛。

### login.blade.php

利用するテンプレート、POSTのaction先をadmin.*以下に変更。

```diff:resources/views/admin/auth/login.blade.php
+@extends('layouts.admin')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Login') }}</div>

                <div class="card-body">
+                   <form method="POST" action="{{ route('admin.login') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="email" class="col-sm-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required autofocus>

                                @if ($errors->has('email'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </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{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>

                                @if ($errors->has('password'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('password') }}</strong>
                                    </span>
                                @endif
                            </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">
                                    {{ __('Login') }}
                                </button>

                                <a class="btn btn-link" href="{{ route('password.request') }}">
                                    {{ __('Forgot Your Password?') }}
                                </a>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
```

### register.blade.php

利用するテンプレート、POSTのaction先をadmin.*以下に変更。

```diff:resources/views/admin/auth/register.blade.php
+@extends('layouts.admin')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Register') }}</div>

                <div class="card-body">
+                    <form method="POST" action="{{ route('admin.register') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>

                            <div class="col-md-6">
                                <input id="name" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" value="{{ old('name') }}" required autofocus>

                                @if ($errors->has('name'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('name') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>

                                @if ($errors->has('email'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </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{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>

                                @if ($errors->has('password'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('password') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>

                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Register') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

```

とりあえず以上。

## 動作確認

```bash
php artisan serve
```

## 今後やること

* usersとadminでセッションを分ける
* 3つ以上のテーブルで試す
* api認証との共存・すみわけ