Laravel8 laravel / uiでマルチ認証を実装

# 完成イメージ

ユーザー種別は以下２種類

- 管理者(administorator)
- スタッフ(staff)

【ER図】

![ER図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1123007/8581757d-88dd-1514-de77-d811ee687a1a.png)

【補足】

- DB関連のファイル群 →　migration・model・seeder(factory)などの準備はされているものとする。
- ログインに必要な処理が記述するControllerには
`administrator/LoginController.php`
と
`staff/LoginController.php`
を使用することを前提とする。
- ユーザー種別によって「ログイン画面」「ログイン後画面」「ログアウト後画面」を分けるようなイメージで実装する
- 新規登録、パスワードリセットなどは行わず、単純にマルチログイン処理のみを必要とする前提で話を進めます

#1. 実装〜Laravel/uiの設定〜

##1-1. Laravel/uiをcomposerにインストールする

```bash
#Auth機能のインストールします
composer require laravel/ui
```

※もしインストール出来なかったら↓

```bash
#バージョン指定までしてインストールする
composer require laravel/ui:2.0
```

##1-2. 認証機能のセットアップをする

```bash
#　認証機能のセットアップします  
php artisan ui vue --auth
```

##1-3. コンパイル

```bash
npm install
npm run dev
```

#2. 実装〜ファイル修正〜

##2-1.不要なファイルの削除
まず、`php artisan ui vue --auth`のコマンドを叩いた時に生成されるファイルから不要なファイルを消していきます。
なお、今回は

- ControllerやViewのファイルは`Administrator`と`Staff`それぞれで個別に作る
- パスワードリセットや新規登録の機能を作らない

という条件のもと進めます。そのため以下のファイルが今回不要となるので消します。 

- `src/app/Http/Controllers/Auth/`以下全ファイル
- `src/app/Http/Controllers/HomeController.php`
- `src/database/migrations/2014_10_12_100000_create_password_resets_table.php`
- `src/resources/views/auth/以下全ファイル`
- `src/resources/views/home.blade.php`

※`src/resources/js/components/ExampleComponent.vue`はコンパイルエラーの元になるので消してはいけません！

##2-2. ファイルの追加・編集

### src/app/Auth/UserProvider.php（新規作成）
このファイルでログイン時にDB登録されている暗号化されたパスワードの複合を行う

```src/app/Auth/UserProvider.php 
<?php

namespace App\Auth;

use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Support\Facades\Crypt;

class UserProvider extends EloquentUserProvider
{
    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */
    public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];
        return $plain == Crypt::decrypt($user->getAuthPassword());
    }
}
```

### src/app/Providers/AuthServiceProvider.php（既存修正）

```src/app/Providers/AuthServiceProvider.php
<?php

namespace App\Providers;

use App\Auth\UserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Models\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
        // ↓のdb_nameには、後でsrc/config/auth.phpで設定するproviderをセットします
        $this->app['auth']->provider('db_name', function ($app, array $config) {
            return new UserProvider($app['hash'], $config['model']);
        });
    }
}
```

### src/config/auth.php（既存修正）

認証の種類を定義する場所です。
以下の３箇所をそれぞれ修正します。

```src/config/auth.php

//---- guards ここから ----//
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'administrators' => [
            'driver' => 'session',
            'provider' => 'administrators',
        ],
        'staffs' => [
            'driver' => 'session',
            'provider' => 'staffs',
        ],
    ],
//---- guards ここまで ----//

//---- providers ここから ----//
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'administrators' => [
            'driver' => 'task_management',
            'model' => App\Models\User::class,
        ],
        'staffs' => [
            'driver' => 'task_management',
            'model' => App\Models\User::class,
        ],
    ],

//---- providers ここまで ----//

//---- passwords ここから ----//

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
        'administrators' => [
            'provider' => 'administrators',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
        'staffs' => [
            'provider' => 'staffs',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

//---- passwords ここまで ----//
```


### src/app/Http/Middleware/CheckAdministratorsAuthenticate.php（新規作成）

```src/app/Http/Middleware/CheckAdministratorsAuthenticate.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class CheckAdministratorsAuthenticate
{
    /**
     * The authentication factory instance.
     *
     * @var \Illuminate\Contracts\Auth\Factory
     */
    protected $auth;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Factory  $auth
     * @return void
     */
    public function __construct(Auth $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (Auth::guard('administrators')->check()) {
            return redirect('/admin/login');
        }

        return $next($request);
    }
}
```

### src/app/Http/Middleware/CheckStaffsAuthenticate.php（新規作成）


ログインしてる時に/loginにアクセスしてきた時のリダイレクト先を指定する。

```src/app/Http/Middleware/CheckAdministratorsAuthenticate.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class CheckAdministratorsAuthenticate
{
    /**
     * The authentication factory instance.
     *
     * @var \Illuminate\Contracts\Auth\Factory
     */
    protected $auth;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Factory  $auth
     * @return void
     */
    public function __construct(Auth $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (Auth::guard('administrators')->check()) {
            return redirect('/admin/login');
        }

        return $next($request);
    }
}
```

```task_management_system/src/app/Http/Middleware/CheckStaffsAuthenticate.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class CheckStaffsAuthenticate
{
    /**
     * The authentication factory instance.
     *
     * @var \Illuminate\Contracts\Auth\Factory
     */
    protected $auth;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Factory  $auth
     * @return void
     */
    public function __construct(Auth $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (Auth::guard('staffs')->check()) {
            return redirect('/staff/login');
        }

        return $next($request);
    }
}
```

### src/app/Http/Kernel.php（既存修正）

上記で作成した２つのファイルを記述する

```src/app/Http/Kernel.php
protected $routeMiddleware = [
     //一番最後に追加
    'administrators' => App\Http\Middleware\CheckAdministratorsAuthenticate::class,
    'staffs' => App\Http\Middleware\CheckStaffsAuthenticate::class,
];
```

### src/routes/web.php（既存修正）

ルーティングを記述
※ログインしていない状態でアクセスできるように注意

```src/routes/web.php
//管理者
Route::prefix('/admin')->namespace('Admin')->group(function () {
    Route::get('login/',    ['as' => 'adminLogin', 'uses' => 'LoginController@index']);
    Route::post('login/',   'LoginController@login');
    Route::get('logout/',   'LoginController@logout');
});

//スタッフ
Route::prefix('/staff')->namespace('Staff')->group(function () {

    Route::get('login/',    ['as' => 'staffLogin', 'uses' => 'LoginController@index']);
    Route::post('login/',   'LoginController@login');
    Route::get('logout/',   'LoginController@logout');
});
```

### src/app/Http/Controllers/Admin/LoginController.php（新規作成）
### src/app/Http/Controllers/Staff/LoginController.php（新規作成）

各ログインコントローラーにログイン処理に必要な処理を書きます。

```src/app/Http/Controllers/Admin/LoginController.php

   
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
    use AuthorizesRequests,
        DispatchesJobs,
        ValidatesRequests,
        AuthenticatesUsers;

    protected $maxAttempts  = 50;   // ログイン試行回数（回）
    protected $decayMinutes = 0;   // ログインロックタイム（分）
    protected $redirectTo   = '/admin/schedule/'; //ログイン後に表示する画面


    /**
     * 認証を無効にする画面を設定
     */
    public function __construct()
    {
        $this->middleware('auth:administrators')->except(['index', 'login']);
    }

    /**
     * 使用する認証を設定
     *
     * @return void
     */
    protected function guard()
    {
        return Auth::guard('administrators');
    }

    /**
     * ログインIDを指定のカラムに変更する
     * （初期値はemail）
     *
     * @return void
     */
    public function username()
    {
        return 'mail';
    }

    /**
     * ログイン画面表示
     * @return view
     */
    public function index()
    {
        return view('admin.login.index');
    }

    /**
     * 認証の条件を設定
     * ログインID、パスワード、ユーザータイプ、削除日
     *
     * @param Request $request
     * @return void
     */
    public function attemptLogin(Request $request)
    {
        return $this->guard()->attempt([
            'mail' => $request->input('mail'),
            'password' => $request->input('password'),
            'type' => config('const.user.authority.administrator'),
            'status' => config('const.user.status.on'),
            'deleted_at' => null,
        ]);
    }

    /**
     * 認証前のバリデーション
     *
     * @param Request $request
     * @return void
     */
    protected function validateLogin(Request $request)
    {

        if (!isset($request->mail)) {
            throw ValidationException::withMessages([
                $this->username() => [trans('auth.mail_req')],
            ]);
        }

        if (!isset($request->password)) {
            throw ValidationException::withMessages([
                $this->username() => [trans('auth.password_req')],
            ]);
        }
        $this->validate($request, [
            $this->username() => 'required|string',
            'password' => 'required|string',
        ]);
    }

    /**
     * ログアウト
     * @return redirect
     */
    public function logout()
    {
        User::flushEventListeners();
        $this->guard('administrators')->logout();
        return redirect()->to('/admin/login');
    }
}
```

```src/app/Http/Controllers/Staff/LoginController.php
<?php

namespace App\Http\Controllers\Staff;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
    use AuthorizesRequests,
        DispatchesJobs,
        ValidatesRequests,
        AuthenticatesUsers;

    protected $maxAttempts  = 50;   // ログイン試行回数（回）
    protected $decayMinutes = 0;   // ログインロックタイム（分）

    /**
     * ログイン後に表示する画面
     */
    protected function redirectTo()
    {
        if (!Auth::guard('staffs')->check()) {
            return redirect('/staff/schedule');
        }

        $user = Auth::guard('staffs')->user();
        return route('daily', ['userId' => $user->id]);
    }

    /**
     * 認証を無効にする画面を設定
     */
    public function __construct()
    {
        $this->middleware('auth:staffs')->except(['index', 'login']);
    }

    /**
     * 使用する認証を設定
     *
     * @return void
     */
    protected function guard()
    {
        return Auth::guard('staffs');
    }

    /**
     * ログインIDを指定のカラムに変更する
     * （初期値はemail）
     *
     * @return void
     */
    public function username()
    {
        return 'mail';
    }

    /**
     * ログイン画面表示
     * @return view
     */
    public function index()
    {
        return view('staff.login.index');
    }

    /**
     * 認証の条件を設定
     * ログインID、パスワード、ユーザータイプ、削除日
     *
     * @param Request $request
     * @return void
     */
    public function attemptLogin(Request $request)
    {
        return $this->guard()->attempt([
            'mail' => $request->input('mail'),
            'password' => $request->input('password'),
            'type' => config('const.user.authority.staff'),
            'status' => 1,
            'deleted_at' => null,
        ]);
    }

    /**
     * 認証前のバリデーション
     *
     * @param Request $request
     * @return void
     */
    protected function validateLogin(Request $request)
    {
        if (!isset($request->mail)) {
            throw ValidationException::withMessages([
                $this->username() => [trans('auth.mail_req')],
            ]);
        }
        
        if (!isset($request->password)) {
            throw ValidationException::withMessages([
                $this->username() => [trans('auth.password_req')],
            ]);
        }
        $this->validate($request, [
            $this->username() => 'required|string',
            'password' => 'required|string',
        ]);
    }

    /**
     * ログアウト
     * @return redirect
     */
    public function logout()
    {
        User::flushEventListeners();
        $this->guard('staffs')->logout();
        return redirect()->to('/staff/login');
    }
}
```

### src/app/Libs/Password.php

パスワードの暗号化、復元のためのUtilファイル

```src/app/Libs/Password.php
<?php

namespace App\Libs;

use Illuminate\Support\Facades\Crypt;

/**
 * Description of Password
 *
 * @author hasegawa
 */
class Password
{

    /**
     * パスワードを暗号化します
     * @param string $plainPassword 生のパスワード
     * @return string 暗号化されたパスワード
     */
    public static function encrypt($plainPassword)
    {
        return Crypt::encrypt($plainPassword);
    }

    /**
     * 暗号化されたパスワードを複合します
     * @param string $password 暗号化されたパスワード
     * @return string 複合されたパスワード
     */
    public static function decrypt($password)
    {
        return Crypt::decrypt($password);
    }

    /**
     * 生のパスワードと暗号化されたパスワードが正しいかチェックします
     * @param string $plainPassword 生のパスワード
     * @param string $password 暗号化されたパスワード
     * @return boolean 一致する場合はtrue、そうでない場合はfalseを返します
     */
    public static function isCorrect($plainPassword, $password)
    {
        return $plainPassword === static::decrypt($password);
    }

    public static function generatePassword($length = 8)
    {
        //vars
        $pwd         = array();
        $pwd_strings = array(
            "sletter" => range('a', 'z'),
            "cletter" => range('A', 'Z'),
            "number"  => range('0', '9'),
        );

        //logic
        while (count($pwd) < $length) {
            // 4種類必ず入れる
            if (count($pwd) < 3) {
                $key = key($pwd_strings);
                next($pwd_strings);
            } else {
                // 後はランダムに取得
                $key = array_rand($pwd_strings);
            }
            $pwd[] = $pwd_strings[$key][array_rand($pwd_strings[$key])];
        }
        // 生成したパスワードの順番をランダムに並び替え
        shuffle($pwd);

        return implode($pwd);
    }

}
```


### resources/lang/ja（新規作成）

resources/lang/enをコピー→auth.phpにログインに関するバリデーションメッセージを作成する

```src/resources/lang/ja/auth.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Language Lines
    |--------------------------------------------------------------------------
    |
    | The following language lines are used during authentication for various
    | messages that we need to display to the user. You are free to modify
    | these language lines according to your application's requirements.
    |
    */

    'failed'           => 'ログインID(メールアドレス)または、パスワードが違います',
    'failed_required'  => 'ログインID(メールアドレス)、パスワードは必須です',
    'failed_mail'      => '入力されたログインID(メールアドレス)は存在しません',
    'failed_status'    => 'このアカウントは有効ではありません',
    'throttle'         => '何度もログインに失敗したため、:seconds秒後に再度お試しください。',
    'mail_req'         => 'ログインID(メールアドレス)は必須入力です',
    'password_req'     => 'パスワードは必須入力です',

];
```

### src/app/User.php

ユーザーモデルにパスワード暗号化・複合化に関するアクセサとミューテタを作成する。
今回は暗号化しているのでgetAttributeで複合化、SetAttributeで暗号化。

```src/app/User.php
    /**
     * 復号したパスワードを返します
     *
     * @return string
     */
    public function getPlainPasswordAttribute()
    {
        return isset($this->password) ? Crypt::decrypt($this->password) : '';
    }

    /**
     * 平文パスワードを暗号化してpasswordにセットします
     * @param string $plainLoginPassword 平文パスワード
     */
    public function setPlainPasswordAttribute($plainLoginPassword)
    {
        $encryptPw = Crypt::encrypt($plainLoginPassword);
        $this->password = $encryptPw;
    }
```

### src/app/Exceptions/Handler.php

認証切れの場合のリダイレクト先の指定する

```src/app/Exceptions/Handler.php
    /**
     * 認証切れの場合のリダイレクト先設定
     * @param type $request
     * @param AuthenticationException $exception
     * @return type
     */
    protected function unauthenticated($request, AuthenticationException $exception)
    {
        if ($request->expectsJson()) {
            return response()->json(['error' => 'Unauthenticated.'], 401);
        }
                
        switch ($exception->guards()[0]) {
            case 'administrators':
                return redirect()->guest(route('login'));
            case 'groups':
                return redirect()->guest(route('groupLogin'));
            case 'personals':
                return redirect()->guest(route('personalLogin'));
        }
    }
```

### 必要に応じて、、

ログイン時にlogin_ipなどをDBに保存しに行く処理

src/app/Listeners/LogSuccessfulLogin.php```

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use App\User;
use Illuminate\Http\Request;

/**
 * ログイン成功時のイベントリスナー
 */
class LogSuccessfulLogin
{

    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     * @param  Login  $event
     * @return void
     */
    public function handle(Login $event)
    {
        $request = app('request');
        User::flushEventListeners();

        /* @var $user User */
        $user                          = $event->user;
        $user->login_server            = $request->server('HTTP_HOST');
        $user->login_user_agent        = $request->server('HTTP_USER_AGENT');
        $user->login_referer           = $request->server('HTTP_REFERER');
        $user->login_remote_host       = $request->server('REMOTE_HOST');
        $user->login_remote_ip_address = $this->getUserIP($request);
        $user->logined_at              = date('Y-m-d H:i:s');
        $user->timestamps              = false;
        $user->save();
    }

    private function getUserIP(Request $request)
    {
        $client  = $request->server('HTTP_CLIENT_IP');
        $forward = $request->server('HTTP_X_FORWARDED_FOR');
        $remote  = $request->server('REMOTE_ADDR');

        if (filter_var($client, FILTER_VALIDATE_IP)) {
            $ip = $client;
        } elseif (filter_var($forward, FILTER_VALIDATE_IP)) {
            $ip = $forward;
        } else {
            $ip = $remote;
        }

        return $ip;
    }
}
```

```src/app/Providers/EventServiceProvider.php
    protected $listen = [//この配列に追記する
        'Illuminate\Auth\Events\Login' => [
            'App\Listeners\LogSuccessfulLogin',
        ],
    ];
```

## 最後に

これでログイン処理に関しては実装できているかと思います。
Viewなどのファイルは各自で用意して動作の確認を行ってください。