Laravelで複数テーブルを使ったログイン認証を実装する

# ユースケース
ユーザーのモデルが複数あり、それぞれ別テーブルで管理されている場合のログイン処理

▼システムの利用ユーザー
管理者：Adminモデル、adminテーブル
教師：Teacherモデル、teacherテーブル
親：Parentモデル、parentテーブル

▼各ユーザーのIDフォーマット
管理者：6桁の数字+A  （ex. 123456A）
教師：6桁の数字+T    （ex. 123456T）
親：6桁の数字+P      （ex. 123456P）

末尾に英文字を付与することでIDが重複することはない。

以上の要件で、同じログイン画面から、入力されたIDの末尾を元にモデルを判別し、
ログイン処理を行い、各ユーザーが使用する画面へと遷移させる処理を実装する。

バージョンはLaravel5.2です。


## 下準備
AuthControllerを実装。
マルチ認証で検索すると、各モデルごとに認証のコントローラーを作成する例がいくつか出てくるが、
1つでも十分可能なので1つにまとめる。


▼まずはログイン後にリダイレクトするURLを定義する。

```php:AuthController.php
protected $admin_redirectTo = '/admin/home';
protected $teacher_redirectTo = '/teacher/home';
protected $parent_redirectTo = '/parent/home';
```

▼ビジネスロジックはサービス層の別クラスに書くため以下も定義しておく。

```php:AuthController.php
protected $authService;

public function __construct(AuthService $authService) {
    $this->authService = $authService;
}
```

▼ログイン画面のルーティング

```php:routes/php
Route::get('/login', 'AuthController@getLogin')->name('login');
Route::post('/login', 'AuthController@postLogin')->name('login');
```

▼コントローラーの処理

```php:AuthController.php
public function getLogin() {
    return view('auth.login');
}

public function postLogin(Request $req) {
    $isAuthenticated = $this->authService->authorize($req->all());
    if($isAuthenticated) {
        $role = $this->authService->getUserRole($req->input('user_id'));
        switch($role) {
            case 'admins':
                return redirect($this->admin_redirectTo);
            case 'teachers':
                return redirect($this->teacher_redirectTo);
            case 'parents':
                return redirect($this->parent_redirectTo);
        }
    } else {
        return redirect()->back()->with('error_msg', config('errors.auth_failed'));
    }
}
```

IDとパスワードが一致しているかを`AuthService#authorize()`でチェックする。
認証できればtrueが返ってくる。

そして` getUserRole() `で入力されたIDの末尾からユーザーのタイプを判別して
それぞれの画面へとリダイレクトさせる。

```php:AuthService
public function getUserRole($user_id) {
    if(ends_with($user_id, 'A')) {
        return 'admins';
    } else if(ends_with($user_id, 'T')) {
        return 'teachers';
    } else if(ends_with($user_id, 'P')) {
        return 'parents';
    }
    return 'unknown';
}
```

$roleの値にsが付いてるのは後々のためです。
単数形の方がしっくりくる気がしますけどね。

## 認証処理authorize()の中身

入力されたユーザーIDから、ユーザータイプを判別、
それぞれのタイプごとの認証処理を行わせる。

```php:AuthService
public function authorize($data) {
    $user_id = $data['user_id'];
    $role = self::getUserRole($user_id);
    $password = $data['password'];
    if(Auth::guard($role)->attempt(['id' => $user_id, 'password' => $password, 'is_deleted' => 0])) {
        $user = Auth::guard($role)->user();
        session()->put('user_id', $user->id);
        session()->put('user_name', $user->name);
        session()->put('user_role', $role);
        return true;
    } 
    return false;
}
```
・ユーザーIDからレコードを検索
・パスワードが合ってるか確かめる
・削除済みでないか確かめる（is_deletedカラムの0,1で判定）
・合ってたらセッションにユーザーIDなどを保存
・認証が成功した証拠にtrueを返す
といった流れ。


レコードをどのテーブルから検索するかを指定しているのが`Auth::guard()`の引数。

## Auth::guard()とは

guardとは。
config\auth.phpを見てください。

デフォルトでは「セッションストレージとUser Eloquentモデルを使って認証を行うよ」というルールが定義されています。

要はここに適当な名前を付けて追加でルールを定義してやるだけ。
`Auth::guard()`の引数にその名前を与えればそのルールで認証処理を行ってくれるわけです。

```php:auth.php
'guards' => [
    'admins' => [
        'driver' => 'session',
        'provider' => 'admins',
    ],
    'teachers' => [
        'driver' => 'session',
        'provider' => 'teachers',
    ],
    'parents' => [
        'driver' => 'session',
        'provider' => 'parents',
    ],
],
```

どのモデルを使うかは別途prodviderに定義します。

```php:auth.php
'providers' => [
    'admins' => [
        'driver' => 'eloquent',
        'model' => App\Models\Admin::class,
    ],
    'teachers' => [
        'driver' => 'eloquent',
        'model' => App\Models\Teacher::class,
    ],
    'parents' => [
        'driver' => 'eloquent',
        'model' => App\Models\Parent::class,
    ],
],
```

なんかguardの名前にはsをつけるのが通例のようです。さっきの`getUserRole`でsをつけたのはこのためです。

これでログイン処理ができました。
`Auth::guard('teachers')`と指定するとTeacherクラス(teacherテーブル)からユーザーを検索する認証処理が実行されます。


## モデルを認証用のモデルとして作成する。

Eloquentモデルなら何でもかんでも認証に使えるわけではありません。
以下のAdminモデルでは先ほどの`Auth::guard('admins')`はエラーになります。

```php:Admin.php
use Illuminate\Database\Eloquent\Model;

class Admin extends Model {

}
```

認証に使うモデルは`Illuminate\Foundation\Auth\User`を継承する必要があるそうです。
デフォルトで用意されているUserクラスの形をそのままコピペしましょう。

```php:Admin.php
use Illuminate\Foundation\Auth\User as Authenticatable;

class Admin extends Authenticatable
{

}

```

ちなみに今回のようにプライマリーキーに整数型の自動採番を用いない場合は、
モデルクラスに`public $incrementing = false;`を記述する必要があります。
自動採番だとIDが重複する可能性があるため、今回の実装ではテーブルの特定ができません。
複数のモデルで同じIDが存在してしまう場合は、テーブルを特定（指定）するためにログインページを複数作る必要があります。
今回の例はIDの末尾でモデルが判断できるため1つのログインページで事足ります。


## ユーザーごとにアクセス制限をかける

認証ができたのであとはルーティングです。
管理者が使う画面には当然他のタイプのユーザーがアクセスできないようにしなければなりません。

画面にアクセスしたときに権限を見て403画面を表示するでもいいのですが、
すっきりさせたいので全てroutes.phpで制御します。

権限がない画面だった場合はデフォルトのホーム画面へとリダイレクトさせます。
先のログイン認証後にリダイレクトしたページと同じです。

```php:routes.php
// 管理者のみアクセスできる
Route::group(['middleware' => ['auth:admins']], function() {
    Route::get('/', function () {
        return redirect()->route('admin_home');
    });
    
    // 管理者一覧
    Route::get('/admin/index', 'AdminController@index')->name('admin_home');
    // 管理者検索
    Route::get('/admin/search', 'AdminController@search');
    // 管理者詳細
    Route::get('/admin/detail/{id}', 'AdminController@detail');
        
});

// 教師のみアクセスできる
Route::group(['middleware' => ['auth:teachers']], function() {
    Route::get('/', function () {
        return redirect()->route('teacher_home');
    });
    
    // 生徒一覧
    Route::get('/student/info', 'StudentController@getHomeInfo')->name('teacher_home');
    // 生徒検索
    Route::get('/student/search', 'StudentController@search');
    // 生徒詳細
    Route::get('/student/detail/{id}', 'StudentController@detail');
});

```

`Route::group(['middleware' => '['auth:guard']])`で囲むと、そのモデルで認証済みでなければ
指定されたURLにリダイレクトされます。ではURLはどこで指定しているんでしょうか？
上記の例で言えば、管理者でログインした状態で生徒一覧の画面にアクセスした場合は、
どこにリダイレクトされるの？

## 認証済みでない場合の処理を記述するのがMiddleware\Authenticate

Kernel.phpを見てもらえばわかるのですが、デフォルトの認証ミドルウェアである`auth`とひもづいているのがこの
`\App\Http\Middleware\Authenticate.php`になります。
ルーティングでミドルウェアに`auth`を指定すると先にこのクラスの処理が走ります。

```Authenticate.php
public function handle($request, Closure $next, $guard = null)
{
    if (Auth::guard($guard)->guest()) {
        if ($request->ajax()) {
            return response('Unauthorized.', 401);
        } else {
            $user_id = session('user_id');
            if(is_null($user_id)) {
                return redirect()->guest('login');
            } else {
                if(ends_with($user_id, 'A')) {
                    return redirect()->route('admin_home');
                } else if(ends_with($user_id, 'T')) {
                    return redirect()->route('teacher_home');
                } else if(ends_with($user_id, 'P')) {
                    return redirect()->route('parent_home');
                }
            }
        }
    }
}
```
`Auth::guard($guard)->guest()`がtrueの場合はつまり、指定されたguardで認証済みでない場合です。
Ajaxはまあ無視するとして、ここでもセッションに保存したユーザーIDを使います。
末尾からログイン中のユーザ―タイプを識別し、それぞれのホーム画面へとリダイレクトさせています。

そもそもログインしていない場合はセッションにユーザーIDが存在しないのでログイン画面へと飛ばしています。



さて、逆のパターンもやらなきゃです。
ログインしてたらアクセスできない画面を設定します。
ログイン画面とかですね。

## 認証済みの場合の処理を記述するのがMiddleware\RedirectIfAuthenticated

クラス名からも分かるように「認証済みであればここにリダイレクトしますよ」の設定をするクラス。

```php:RedirectIfAuthenticated.php
public function handle($request, Closure $next, $guard = null)
{
    if (Auth::guard($guard)->check()) {
        switch($guard) {
            case 'admins':
                return redirect()->route('admin_home');
            case 'teachers':
                return redirect()->route('teacher_home');
            case 'parents':
                return redirect()->route('parent_home');
        }
    }
    return $next($request);
}
```

さっきより楽ですね。guardが分かっているので、セッションの値を見るなどのチェックが不要になります。
さて、このクラスはどうやったら呼ばれるのか。

```php:routes.php
Route::group(['middleware' => ['guest:admins, teachers, parents']], function() {
    Route::get('/', function () {
        return route('login');
    });
    // ログイン
    Route::get('/login', 'AuthController@getLogin')->name('login');
    Route::post('/login', 'AuthController@postLogin')->name('login');
});
```

ミドルウェアに`guest`を指定します。
肝は適用するguardの種類を全て指定すること。
上記の例だとadminでログインしていてもteacherでログインしていてもparentでログインしていても
ログイン画面にはアクセスできないようにしています。
アクセスしようとすると`RedirectIfAuthenticated`で定義したホーム画面へと飛ばされます。


* * *

<br>
複数ユーザーのログインを1つのシステム内でなるべく簡潔に済ませたかったので
こんな感じになりました。この手のやり方はあまり情報がなくほとんど自前で考えながら書いたので
もっといいやり方があるかもしれません。


以上です。
ご査収ください。