Laravel 5.4/5.5 で Multi-Auth を実装する

追記: このあと 5.5 にアップデートしましたが多分そのままいい感じに動いてます。

- - -

以下の記事がとても良くかかれていて、これに沿って実装するだけで殆どうまくいった。

http://www.84kure.com/blog/2016/06/06/laravel-5-3%E3%81%A7multi-auth%E3%82%92%E4%BD%BF%E3%81%86/

がしかし、一部 5.4 での記載の違いや追加でやっておきたい変更、改良出来そうな手順があったのでほぼ自分用のメモとして記載。

認証追加前の準備
================

Laravel プロジェクトのセットアップ

```shell
composer create-project --prefer-dist laravel/laravel multiauthsample
```

を行うとデフォルトで入っている User Model がある前提。また Auth scaffold を作成しておく。

```shell
php artisan make:auth
```

追加認証用の Model
==================

追加認証に用いる Model を作成する。

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

`-m` オプションでマイグレーションファイルも同時に作成出来る。この Admin モデルの実装とマイグレーションの内容、つまり Model の構成は User Model と同様に実装しておく。

認証設定
========

`config/auth.php` を以下のように修正。

```diff
--- a/config/auth.php
+++ b/config/auth.php
@@ -40,6 +40,10 @@ return [
             'driver' => 'session',
             'provider' => 'users',
         ],
+        'admin' => [
+            'driver' => 'session',
+            'provider' => 'admins',
+        ],

         'api' => [
             'driver' => 'token',
@@ -69,6 +73,10 @@ return [
             'driver' => 'eloquent',
             'model' => App\User::class,
         ],
+        'admins' => [
+            'driver' => 'eloquent',
+            'model' => App\Admin::class,
+        ],

         // 'users' => [
         //     'driver' => 'database',
@@ -97,6 +105,11 @@ return [
             'table' => 'password_resets',
             'expire' => 60,
         ],
+        'admins' => [
+            'provider' => 'admins',
+            'table' => 'password_resets',
+            'expire' => 60,
+        ],
     ],

 ];

```

`'guards'` 内の要素について、この項目は実装内で guard インスタンスを取得する際 `Auth::guard('admin');` の指定に用いるので、名称が異なる同じ内容が複数存在していても問題無い。
`'guards'` 内に元々ある `'web'` が User 用の認証に該当している。参考にした記事では同じ内容を `'user'` として追加していて、分かり易くて良さそうだった。

ログイン・ログアウト実装
========================

Auth scaffold 内にはログイン・ログアウト、ユーザー登録、パスワードリマインダの機能が入っているが、最低限のログイン・ログアウトのみ実装。ユースケースにもよるが、管理者的な権限の認証を実装する場合は Web に新規登録画面を設けるよりは artisan タスク等によってユーザーを発行してしまってもいいのではないかと思う。

`app/Http/Controllers/Auth/LoginController.php` を `app/Http/Controllers/Admin/Auth/LoginController.php` としてコピーし、以下のように実装。Auth/ 内の他のファイルはログイン・ログアウトのみ実装の場合は不要。

```diff
diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Admin/Auth/LoginController.php
index b2ea669..ae4d412 100644
--- a/app/Http/Controllers/Auth/LoginController.php
+++ b/app/Http/Controllers/Admin/Auth/LoginController.php
@@ -1,10 +1,15 @@
 <?php

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

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

+use Auth;
+
+/**
+ * From app/Http/Controllers/Auth/LoginController.php
+ */
 class LoginController extends Controller
 {
     /*
@@ -25,7 +30,7 @@ class LoginController extends Controller
      *
      * @var string
      */
-    protected $redirectTo = '/home';
+    protected $redirectTo = '/admin/home';

     /**
      * Create a new controller instance.
@@ -34,6 +39,18 @@ class LoginController extends Controller
      */
     public function __construct()
     {
-        $this->middleware('guest')->except('logout');
+        $this->middleware('guest:admin')->except('logout');
+    }
+
+    // http://www.84kure.com/blog/2016/06/06/laravel-5-3%E3%81%A7multi-auth%E3%82%92%E4%BD%BF%E3%81%86/
+
+    public function showLoginForm()
+    {
+        return view('admin.auth.login'); //管理者ログインページのテンプレート
+    }
+
+    protected function guard()
+    {
+        return Auth::guard('admin'); //管理者認証のguardを指定
     }
 }

```

後述するログイン済ユーザーのリダイレクトに対応する為に `$this->middleware('guest:admin')->except('logout');` のように guest Middleware に引数 `admin` を渡しておく。そして管理者ログインページ用のテンプレートを上記 `view()` で指定した先に作成する。基本的には `resources/views/auth/login.blade.php` のコピーで問題無いが、form の method、action 属性は確認する。

ログイン済ユーザーのリダイレクト
------------------------

追加認証でのログイン済ユーザーがログインページにアクセスした場合、guest Middleware による処理で一括で `'/home'` にリダイレクトされる。 `app/Http/Kernel.php` によると guest Middleware の実装は `app/Http/Middleware/RedirectIfAuthenticated.php` なのでこれを修正する。

```diff
diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Mi
index e4cec9c..719e3a5 100644
--- a/app/Http/Middleware/RedirectIfAuthenticated.php
+++ b/app/Http/Middleware/RedirectIfAuthenticated.php
@@ -17,8 +17,18 @@ class RedirectIfAuthenticated
      */
     public function handle($request, Closure $next, $guard = null)
     {
+        $redir = '/home';
+        switch ($guard) {
+        case "admin":
+            $redir = '/admin/home';
+            break;
+        default:
+            $redir = '/home';
+            break;
+        }
+
         if (Auth::guard($guard)->check()) {
-            return redirect('/home');
+            return redirect($redir);
         }

         return $next($request);
```

元々 `$guard` という引数を取る作りになっていたので、これによってリダイレクト先を変えるようにする。

未認証時のリダイレクト
======================

認証が必要なページに未認証でアクセスした場合のリダイレクト。

```diff
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index a747e31..ddf4adc 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -59,6 +59,11 @@ class Handler extends ExceptionHandler
         if ($request->expectsJson()) {
             return response()->json(['error' => 'Unauthenticated.'], 401)
         }
+
+        if (in_array('admin', $exception->guards())) {
+            return redirect()->guest(route('admin::login'));
+        }

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

元からある記載が名前付 route を使用していたので統一して名前付 route `route('admin::login')` を使用。`'admin/login'` 等パス直書きでも問題無い。

ルーティング
============

5.4 ではルーティング用のファイルの構成が変わっている。`routes/web.php` に以下の通り追加。

```diff
diff --git a/routes/web.php b/routes/web.php
index ca66ae6..850ff5f 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -19,3 +19,24 @@ Auth::routes();

 Route::get('/home', 'HomeController@index')->name('home');
+
+Route::prefix('admin')->name('admin::')->group(function() {
+    // For admin auth pages as generated by Auth::routes();
+    // See also: https://stackoverflow.com/a/39197278/4821980
+
+    // Authentication Routes...
+    Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
+    Route::post('login', 'Auth\LoginController@login');
+    Route::post('logout', 'Auth\LoginController@logout')->name('logout');
+
+    // // Registration Routes...
+    // Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
+    // Route::post('register', 'Auth\RegisterController@register');
+
+    // // Password Reset Routes...
+    // Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
+    // Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
+    // Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
+    // Route::post('password/reset', 'Auth\ResetPasswordController@reset');
+
+    Route::get('/', 'AdminController@index');
+    Route::get('/home', 'AdminController@index');
+});
```

前項で使用した名前付 route 用の prefix `admin::` をグループに指定しておく。

これらの route 定義がどこから来ているかというと、この差分よりもう少し上に元々定義されている `Auth::routes();` の実装にある。これが実行されると標準 Auth 用のルート定義が一括で追加されるようになっている。その内容を参考に `admin` 用に変更し、上記の通り実装する。ユーザー登録やパスワードリマインダを実装する場合はコメントアウトしている部分を元に追加することになる。

認証要件の追加
==============

Controller に追加する場合はコンストラクタへ。

```php:AdminController.php
public function __construct()
{
    $this->middleware('auth:admin');
}
```

ルーティンググループでも適用出来る。

```php:routes/web.php
Route::middleware('auth:admin')->group(function () {
    Route::get('/admin/home', 'AdminController@index');
});
```