Laravel5.4で複数アカウント機能毎に別々認証

よいタイトル思いつかなかった・・・
会員制ウェブサイトを構築するとして。
よくある、会員アカウントが利用するページとは別に、運営者のみが利用できる管理画面が必要となる要件。
別物だからとは言っても、データベースとかは一緒だし・・・同じアプリに組み込みたいよね。
そんな時はLaravel5.2以上で実装されたと噂のマルチ認証機能を使えば解決さ。

今回はLaravel5.4で試したよ。

# 管理画面に関する要件
- App\Models\Adminクラスのモデルを管理者アカウント
  - アカウントIDとパスワードで認証
- 管理画面のURLプレフィックスは /admin
- 管理画面のログインページは /admin/login
  - 未認証で管理画面アクセスはログインページへリダイレクト

# 実装方法
## 認証ガード設定
どのように認証を行うかのガード設定がある。
そのガードに、管理者用に認証ガードを追加。
追加したガードがApp\Models\Adminクラスを利用するよう、参照するプロバイダーを合わせて追加。
これを config/auth.php へ追加する。

```php:config/auth.php
return [
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        // adminガードを追加
        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],
    ],

    // プロバイダー設定
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        // adminsプロバイダーを追加
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ],
    ],
];
```
## ミドルウェア
>*--- 2017.12.12追記 ---*
5.5のコードを見てたら、例外処理を受ける所でリダイレクト処理をしているのを発見。
おそらく5.4でも同じだと思うので、例外処理で対応すればこのミドルウェアは対応しなくてよい。

### 作成

追加したガード設定を行って認証判定を行うミドルウェアを作成。
Authファサードからガードを指定して認証判定を行うことができる。
`Auth::guard(string)->check()`メソッドで判定可能。
判定の結果、未認証状態であれば管理画面のログイン画面にリダイレクトさせるようにする。
これで、管理画面専用の認証ミドルウェアが実現する。

```php:app/Http/Middleware/AuthenticateAdmin.php
namespace App\Http\Middleware;

use Closure;
use Auth;

/**
 * 管理画面認証ミドルウェア
 */
class AuthenticateAdmin
{
    /**
     * Handle an incoming request.
     * 
     * @param \Illuminate\Http\Request  $request
     * @param \Closure  $next
     * @return mixed
     */
    public function hanlde($request, Closure $next)
    {
        // adminアカウントの認証がなければ管理画面ログインフォーム画面へリダイレクト
        if (Auth::guard('admin')->check() === false) {
            return redirect()->route('admin.login');
        }

        return $next($request);
    }
}
```
### 登録
作成したミドルウェアをKernelクラスの$routeMiddlewareプロパティに追加。

```php:app\Http\Kernel.php
namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    //他は省略
    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'auth.admin' => \App\Http\Middleware\AuthenticateAdmin::class, // 追加
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    ];
}
```


# モデルの作成
まぁ、初めに作ってしまっても良かったんだけど・・・
Illuminate\Foundation\Auth\User を継承したモデルを作成すれば、認証機能に利用することができる。
メソッドをオーバーライドすることでカスタム可能。
例えば、パスワード保存カラムがpasswordではなくpasswdだった場合を記載。

万一だよ？ないと思うけどハッシュ化されていない平文保存のDBを利用する場合は、`\Hash::make($this->passwd)`を`getAuthPassword()`メソッドの戻り値とすれば対応できる。
まぁ、そんな事あるはずないよね:smirk:

```php:App\Models\Admin
namespace App\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

/**
 * 管理者アカウントモデル
 */
class Admin extends Authenticatable
{
    /**
     * パスワード情報をカスタマイズ
     * @return string
     */
    public function getAuthPassword()
    {
        return $this->passwd;
    }
}
```

# ルーティング設定
これで準備は整ったので、今回作成したミドルウェアを利用するルーティングを作成すればOKさ:laughing:

```php:routes/web.php
// ===== 会員ページ =====
Route::group(['namespace' => 'Member', 'prefix' => 'member', 'as' => 'member.'], function() {
    // ----- ログインページ -----
    Route::group(['namespace' => 'Auth'], function() {
        Route::get('login', 'LoginController@showLoginForm')->name('login');
        Route::post('login', 'LoginController@login')->name('login.post');
    });

    // ----- 認証が必要な会員ページ -----
    Route::group(['middleware' => 'auth'], function() {
        Route::get('/', 'HomeController@index')->name('index');
    });
});


// ===== 管理画面のルーティング =====
Route::group(['namespace' => 'Admin', 'prefix' => 'admin', 'as' => 'admin.'], function() {
    // ----- ログインページ -----
    Route::group(['namepsace' => 'Auth'], function() {
        Route::get('login', 'LoginController@showLoginForm')->name('login');
        Route::post('login', 'LoginController@login')->name('login.post');
    });

    // ----- 管理画面の認証が必要なページ -----
    // ミドルウェア作成ではなく例外処理で対応の場合は ['middleware' => 'auth:admin'] にする
    Route::group(['middleware' => 'auth.admin'], function() {
        Route::get('/', 'DashboardController@index')->name('index');
    });
});
```

# 例外処理（2017.12.12追記）
>専用ミドルウェア作成ではなく例外処理で対応の場合に実施

authミドルウェア設定されているルートにアクセスがあった際に、
非認証状態であれば Illuminate\Auth\AuthenticationException が投げられる。
投げられた例外は App\Exceptions\Handler クラスがキャッチして、unauthenticatedメソッドが呼ばれている。
なので、ここをちょこっと細工すれば、認証ガード別にリダイレクト先を切り分けできる。

ガードの種類をどうやって判定するかは、第二引数に渡されてくるexceptionから取得できる。

*ガード取得参考記事*
https://qiita.com/sawadashota/items/7921b0c9ac622965d7df

```php:app/Exceptions/Handler.php
namespace App\Exceptions;

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

class Handler extends ExceptionHandler
{
    // 他のメソッドは省略
    protected function unauthenticated($request, AuthenticationException $exception)
    {
        if ($request->expectsJson()) {
            return response()->json(['error'=> 'Unauthenticated.'], 401);
        }

        // ここから先を変更する
        // ガードを増やした場合はここのケースに追加していく
        switch($exception->guard()[0]) {
            case 'admin':
                return redirect()->guest(route('admin.login'));
                break;
            default:
                return redirect()->guest(route('member.login'));
                break;
        }
    }
}
```