[Laravel 5.7] フォーム認証処理の仕組み

普通のログインフォームで認証する場合のLaravelの処理を追ってみます。

# Laravelのフォーム認証を構成するもの

`php artisan make:auth`した状態です。

- **コントローラ** (`App\Http\Controllers\Auth\LoginController`)  
継承元のloginメソッドでガードを呼び出します。
- **ガード** (`Illuminate\Auth\SessionGuard`)  
ログイン処理を実行します。その中でプロバイダーを呼び出します。
- **プロバイダー** (`Illuminate\Auth\EloquentUserProvider`)   
入力されたIDのユーザー情報をDBから持ってきたり、その情報と入力されたパスワードを比較したりします。  
DB検索にEloquentモデルを使ってクエリーを走らせます。
- **モデル** (`App\User`)  
プロバイダーがDB検索時に使うEloquentモデル。

## ガード

`Illuminate\Auth\SessionGuard`のattemptメソッドがログイン処理を行っています。

```php:Illuminate\Auth\SessionGuard
/**
 * ログインを試みる
 */
public function attempt(array $credentials = [], $remember = false)
{
    // プロバイダのretrieveByCredentials()でDBからユーザー情報を取得しています。
    // $credentialsは、ログインフォームから送った値で['email' => 'foo@example.com', 'password' => 'secret']のような配列です。
    $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

    // hasValidCredentials()の中で、プロバイダのvalidateCredentials()が実行され、$credentialsと上記で取得した情報を比較しています。
    // 比較がtrueであれば
    if ($this->hasValidCredentials($user, $credentials)) {
        // ログイン成功処理(セッションにユーザー情報をセットしたり)します。
        $this->login($user, $remember);
        return true;
    }
    return false;
}

/**
 * リクエストされた値とDBのユーザー情報を比較する
 */
protected function hasValidCredentials($user, $credentials)
{
    return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
}
```

## プロバイダー

ガードで呼ばれたretrieveByCredentials()とvalidateCredentials()を見てみます。

```php:Illuminate\Auth\EloquentUserProvider
/**
 * リクエストされた情報でDBからユーザー情報を取得する
 */
public function retrieveByCredentials(array $credentials)
{
    // $credentialsが空かpasswordしか無ければreturn
    if (empty($credentials) ||
        (count($credentials) === 1 &&
        array_key_exists('password', $credentials))) {
        return;
    }

    // パスワードを除いた値でクエリーを生成しています。
    // ['email' => 'foo@example.com', 'password' => 'secret']であれば、emailだけ使用されます。
    $query = $this->createModel()->newQuery();
    foreach ($credentials as $key => $value) {
        if (Str::contains($key, 'password')) {
            continue;
        }
        if (is_array($value) || $value instanceof Arrayable) {
            $query->whereIn($key, $value);
        } else {
            $query->where($key, $value);
        }
    }
    // クエリーを実行し該当するユーザーを取得します。
    return $query->first();
}

/**
 * リクエストされたパスワードとDBから取得したパスワードを比較する
 */
public function validateCredentials(UserContract $user, array $credentials)
{
    $plain = $credentials['password'];

    return $this->hasher->check($plain, $user->getAuthPassword());
}
```

## モデル

`App\User`が継承している`Illuminate\Foundation\Auth\User`は、

- `Illuminate\Database\Eloquent\Model`
- `Illuminate\Contracts\Auth\Authenticatable`インターフェース

を継承しています。使用されている`Illuminate\Auth\Authenticatable`トレイトを見てみると、プロバイダーのvalidateCredentials()で使われたgetAuthPassword()などが実装されています。

**プロバイダーに使われるモデルは、`Illuminate\Contracts\Auth\Authenticatable`インターフェースを継承していなければならない**ということです。

# 終わりに

例えば、usersテーブルではなくadministratorsテーブルを認証に使いたい管理画面を実装する際は、

- Authenticatableインターフェースを実装したAdministratorモデルを作る
- config/auth.phpで設定し、authミドルウェア、ルーティングも設定する

以上を行えば良いわけです。プロバイダー、ガードはEloquentモデルとsessionを利用する限り同じように使えます。
ちなみにモデルを用意せずDBにアクセスできるDatabaseUserProviderもあります。