Laravelの認証の動きを詳細に追いかけてみた（ログイン編）


Laravelに標準搭載されている認証の処理について詳細に追いかけていきます。

#バージョン

```php
> php artisan --version
Laravel Framework 6.20.30

// 以下を実行して標準搭載の認証機能が実装されている前提です
> composer require laravel/ui:^1.0 --dev
> php artisan ui vue --auth
```

#ログイン処理の流れを追う
以下の流れで追いかけていきます！

- [0. ルーティングの確認](#0-ルーティングの確認)
- [1. ログインフォームの表示処理](#1-ログインフォームの表示処理)
    - [1.1. ログインフォームの表示処理](#1-ログインフォームの表示処理)
- [2. ログイン処理](#2-ログイン処理)
    - [2.1. ログインリクエストのバリデーション処理](#2-ログイン処理)
    - [2.2. ログイン失敗回数のチェック処理](#2-ログイン処理)
        - [2.2.1. ログイン失敗回数が設定した回数を超えていないかのチェック処理](#2-ログイン処理)
        - [2.2.2. ログイン失敗回数超過によるイベント発火処理](#2-ログイン処理)
        - [2.2.3. ログイン失敗回数超過によるログイン失敗レスポンスの作成処理](#2-ログイン処理)
    - [2.3. ログイン処理](#2-ログイン処理)
        - [2.3.1. ログインの認証処理](#2-ログイン処理)
            - [2.3.1.1. ログイン認証のイベント発行処理 ](#2-ログイン処理)
            - [2.3.1.2. ユーザ情報の取得処理](#2-ログイン処理)
            - [2.3.1.3. 取得したユーザとテーブルに保持するユーザの情報が一致するかのチェック処理](#2-ログイン処理)
            - [2.3.1.4. ログイン処理 ](#2-ログイン処理)
        - [2.3.2 ログイン成功のレスポンス返却処理](#2-ログイン処理)
    - [2.4. ログイン失敗回数を加算する処理](#2-ログイン処理)
    - [2.5. ログイン失敗のレスポンスを返す処理](#2-ログイン処理)

#0. ルーティングの確認
ログイン処理に使用するのは項番を振った2つだけです。

```php
> php artisan route:list 

+--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+
| Domain | Method   | URI                    | Name             | Action                                                                 | Middleware   |
+--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+
|        | GET|HEAD | /                      |                  | Closure                                                                | web          |
|        | GET|HEAD | api/user               |                  | Closure                                                                | api,auth:api |
|        | GET|HEAD | home                   | home             | App\Http\Controllers\HomeController@index                              | web,auth     |
|1       | GET|HEAD | login                  | login            | App\Http\Controllers\Auth\LoginController@showLoginForm                | web          |
|2       | POST     | login                  |                  | App\Http\Controllers\Auth\LoginController@login                        | web          |
|        | POST     | logout                 | logout           | App\Http\Controllers\Auth\LoginController@logout                       | web          |
|        | GET|HEAD | password/confirm       | password.confirm | App\Http\Controllers\Auth\ConfirmPasswordController@showConfirmForm    | web,auth     |
|        | POST     | password/confirm       |                  | App\Http\Controllers\Auth\ConfirmPasswordController@confirm            | web,auth     |
|        | POST     | password/email         | password.email   | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail  | web          |
|        | GET|HEAD | password/reset         | password.request | App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm | web          |
|        | POST     | password/reset         | password.update  | App\Http\Controllers\Auth\ResetPasswordController@reset                | web          |
|        | GET|HEAD | password/reset/{token} | password.reset   | App\Http\Controllers\Auth\ResetPasswordController@showResetForm        | web          |
|        | GET|HEAD | register               | register         | App\Http\Controllers\Auth\RegisterController@showRegistrationForm      | web,guest    |
|        | POST     | register               |                  | App\Http\Controllers\Auth\RegisterController@register                  | web,guest    |
+--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+
```

#####1. GET     | login         | App\Http\Controllers\Auth\LoginController@showLoginForm
ログインフォームを表示するためのルーティングです。

#####2. POST | login          | App\Http\Controllers\Auth\LoginController@login
ログイン処理を行うためのルーティングです。

POSTされたリクエストのバリデーション、実際のログイン処理、POSTされたリクエストでログインできない場合のリダイレクトなど内部的に多くの処理を行っています。

#1. ログインフォームの表示処理

##1.1 ログインフォームの表示処理

`/login`へのGETリクエストが発行されると、`LoginController`の`showLoginForm`メソッドが呼び出されます。

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

`resouce/views/auth`フォルダにある`login.blade.php`を返すだけのものですね。簡単！

###寄り道：トレイトについて

実は`LoginController`のコードを見ると、ほとんど何も記載されていません。

代わりにコントローラ内には下記の記述があります。

```php:LoginController.php
use AuthenticatesUsers; // trait AuthenticatesUsersを使用する宣言
```

上記の`showLoginForm`メソッドを含むほとんどのメソッドは、コントローラ内ではなく`Authenticatesusers`トレイトに実装されています。

> PHP は、コードを再利用するための「トレイト」という仕組みを実装しています。

>トレイトは、PHP のような単一継承言語でコードを再利用するための仕組みのひとつです。 トレイトは、単一継承の制約を減らすために作られたもので、 いくつかのメソッド群を異なるクラス階層にある独立したクラスで再利用できるようにします。 トレイトとクラスを組み合わせた構文は複雑さを軽減させてくれ、 多重継承や Mixin に関連するありがちな問題を回避することもできます。

>トレイトはクラスと似ていますが、トレイトは単にいくつかの機能をまとめるためだけのものです。 トレイト自身のインスタンスを作成することはできません。 昔ながらの継承に機能を加えて、振る舞いを水平方向で構成できるようになります。 つまり、継承しなくてもクラスのメンバーに追加できるようになります。

引用：https://www.php.net/manual/ja/language.oop5.traits.php

`AuthenticatesUsers`トレイトでは`trait AuthenticatesUsers`のような形式でトレイトを宣言しており、トレイトの中にログインに使用されるメソッドをまとめています。

`LoginController`ではトレイトの使用を宣言しているので、継承しなくてもトレイトに記述されたメソッドを`LoginController`に記述されているかのように使用できます。

この後に流れを追っていくメソッド群も基本的に`LoginController`ではなく`AuthenticatesUsers`など他のトレイトに実装されています。

またトレイトのメソッドは`use`宣言している側でオーバーライドできるので、処理をカスタマイズしたい場合はカスタマイズしたいメソッドだけを`use`側で実装することができます。

標準搭載のログイン処理の中で要件に合わせてカスタマイズしたいものがあれば、`LoginController`でそのメソッドだけをオーバーライドすれば簡単にオリジナルのログイン処理が実装できそうです！

#2. ログイン処理

/loginへのPOSTリクエストが発行されると、LoginControllerのloginメソッドが呼び出されます。

やっていることが非常に多いので、細かく分割して見ていきます。

```php:AuthenticatesUsers.php
    // 2. ログイン処理
    public function login(Request $request)
    {
        // 2.1. ログインリクエストのバリデーション処理
        $this->validateLogin($request);

        // 2.2. ログイン失敗回数のチェック処理
        if (method_exists($this, 'hasTooManyLoginAttempts') &&
            $this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }

        // 2.3. ログイン処理
        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }

        // 2.4. ログイン失敗回数を加算する処理
        $this->incrementLoginAttempts($request);

        // 2.5. ログイン失敗のレスポンスを返す処理
        return $this->sendFailedLoginResponse($request);
    }
```

##2.1. ログインリクエストのバリデーション処理
```php
        // 2.1. ログインリクエストのバリデーション処理
        $this->validateLogin($request);
```

POSTリクエストに対してバリデーション処理を行います。
`validateLogin`メソッドを使っていますが、このメソッドも`AuthenticatesUsers`内に実装されています。

```php:AuthenticatesUsers.php
    protected function validateLogin(Request $request)
    {
        $request->validate([
            $this->username() => 'required|string',
            'password' => 'required|string',
        ]);
    }
```

ここではPOSTされた`username()`と`password`に対してバリデーションを行っています。

`username()`は認証に使用する情報を指定するメソッドであり、デフォルトではemailが設定されています。

```php:AuthenticatesUsers.php
    public function username()
    {
        return 'email';
    }
```

つまりはリクエストの`email` `password`それぞれに対して

- required(必須項目であること)
- string（文字列の形式であること）

のチェックを行っているということですね。



バリデーションに成功すれば以降のバリデーション以降の処理が実行され、失敗すれば適切なエラーレスポンスが生成されて`login`メソッドの処理は止まります。

validateメソッドの掘り下げは記事の本旨からは外れますので、使い方の引用にとどめます。

>**バリデーションロジック**

> Illuminate\Http\Requestオブジェクトが提供する、validateメソッドを使います。バリデーションルールに成功すると、コードは通常通り続けて実行されます。逆にバリデーションへ失敗すると例外が投げられ、ユーザーに対し自動的に適切なエラーレスポンスが返されます。伝統的なHTTPリクエストの場合は、リダイレクトレスポンスが生成され、一方でAJAXリクエストにはJSONレスポンスが返されます。

> 実行したいバリデーションルールをvalidateメソッドへ渡します。繰り返しますが、バリデーションに失敗すれば、適切なレスポンスが自動的に生成されます。バリデーションに成功すれば、コントローラは続けて通常通り実行されます。

引用：https://readouble.com/laravel/6.x/ja/validation.html

##2.2. ログイン失敗回数のチェック処理

Webアプリケーションではパスワードリスト攻撃などを回避する目的で、連続でログインに失敗した場合一定時間ログインできない時間を設けることが良くあります。

Laravelの認証においてはこの機能も標準で実装されています。すごい。

```php:AuthenticatesUsers.php
        // 2.2. ログイン失敗回数のチェック処理
        if (method_exists($this, 'hasTooManyLoginAttempts') &&

            // 2.2.1. ログイン失敗回数が設定した回数を超えていないかのチェック処理
            $this->hasTooManyLoginAttempts($request)) {

            // 2.2.2. ログイン失敗回数超過によるイベント発火処理
            $this->fireLockoutEvent($request);

            // 2.2.3. ログイン失敗回数超過によるログイン失敗レスポンスの作成処理
            return $this->sendLockoutResponse($request);
        }
```

###2.2.1. ログイン失敗回数が設定した回数を超えていないかのチェック処理

`hasTooManyLoginAttempts`メソッドは`\vendor\laravel\framework\src\Illuminate\Foundation\Auth\ThrottlesLogins.php`に記載されています。

設定した回数ログインに失敗している場合`hasTooManyLoginAttempts`がtrueを返し、2.2.2～2.2.3の処理が実行されます。

```php:ThrottlesLogins.php
    protected function hasTooManyLoginAttempts(Request $request)
    {
        // 現在のユーザのログイン失敗回数と、ログイン失敗の最大回数を引数にtooManyAttemptsメソッドを呼ぶ
        return $this->limiter()->tooManyAttempts(
            $this->throttleKey($request), $this->maxAttempts()
        );
    }
```

`tooManyLoginAttempts`メソッドも`ThrottlesLogins`トレイトに記載されています。

```php:ThrottlesLogins.php
    public function tooManyAttempts($key, $maxAttempts)
    {
        // 現在のユーザのログイン失敗回数が、設定した回数よりも多い場合
        if ($this->attempts($key) >= $maxAttempts) {
            // キャッシュに:timerというキーが保持されている場合
            if ($this->cache->has($key.':timer')) {
                // ログイン失敗回数超過、trueを返す
                return true;
            }
            // キャッシュに:timerというキーが保持されていない場合、ログイン失敗回数をリセットする
            $this->resetAttempts($key);
        }

        // 現在のユーザのログイン失敗回数が、設定した回数に達していない場合、falseを返して2.3.以降の後続処理を実行する
        return false;
    }
```

`tooManyAttempts`メソッドはまず`attempts($key) `（キャッシュに保存された現在のログイン失敗回数）と、`maxAttempts`（連続ログイン失敗を許容する回数）を比較して、連続でログインに失敗した回数が許容する回数を超えていないかを判定します。

許容されるログインの失敗回数を超過している場合、`if ($this->cache->has($key.':timer'))`でキャッシュに`:timer`という値が保持されているかどうかを見ています。

後述する処理で登場しますが、ログインに連続で失敗した場合、ユーザに対してしばらくログインできないペナルティ時間が設けられます。

そのログインできない時間はキャッシュ上に`:timer`という項目が設けられます（ペナルティ時間が過ぎている場合は`:timer`項目は空になります）。

つまり`if ($this->cache->has($key.':timer'))`では、決められたログインできない時間がもう終わっているか？ということを判定しています。

`($this->cache->has($key.':timer'))`がtrueである（＝まだログインできない時間である）場合は、`tooManyLoginAttempts`メソッドはtrueを返して2.2.2以降のログイン失敗回数が超過していた場合の処理に移行します。

`($this->cache->has($key.':timer'))`がfalseである（＝ログインできない時間が過ぎたので、もうログインできる）場合は、後続の`resetAttempts`メソッドでログイン失敗回数をリセットし、2.3.以降のログイン処理を実行します。

###2.2.2. ログイン失敗回数超過によるイベント発行処理

```php:AuthenticatesUsers.php
// 2.2.2. ログイン失敗回数超過によるイベント発行処理
$this->fireLockoutEvent($request);
```

`fireLockoutEvent`メソッドも`ThrottlesLogin`トレイトに記載。

```php:ThrottlesLogins.php
    protected function fireLockoutEvent(Request $request)
    {
        event(new Lockout($request));
    }
```

`event`ヘルパに新しく作成した`Lockout`イベントのインスタンスを渡すことで、イベントを発行しています。

リスナーを登録しておくことで、ログイン回数超過のイベントの発生を検知し、様々な処理を行うことができます。

イベントの掘り下げについては記事の本旨からは外れますので、割愛します。


###2.2.3. ログイン失敗回数超過によるログイン失敗レスポンスの作成処理

```php:AuthenticatesUsers.php
// 2.2.3. ログイン失敗回数超過によるログイン失敗レスポンスの作成処理
return $this->sendLockoutResponse($request);
```

`sendLockoutResponse`メソッドでは、バリデーションエラーの例外を投げます。

```php:ThrottlesLogins.php
    protected function sendLockoutResponse(Request $request)
    {
        // エラーメッセージに表示する、ログインできない時間の残りを計算する処理
        $seconds = $this->limiter()->availableIn(
            $this->throttleKey($request)
        );

        // バリデーションエラーの例外を投げる
        throw ValidationException::withMessages([
            $this->username() => [Lang::get('auth.throttle', [
                'seconds' => $seconds,
                'minutes' => ceil($seconds / 60),
            ])],
        ])->status(Response::HTTP_TOO_MANY_REQUESTS);
    }
```


エラーメッセージは`username()`に対して`resources\lang\en\auth.php`に記載された`throttle`が設定されます。

```php:auth.php
    // ログイン試行回数が多すぎます。<:seconds>秒後にまた試してください。
    'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
```

`throttle`メッセージの内容を書き換えれば、好きなログイン失敗メッセージを設定できますね。
`minutes(seconds / 60)`変数も標準で用意されているので、分単位での表示も簡単にできそうです。

##2.3. ログイン処理

ようやく実際のログイン処理です！

2.3.1.の認証処理がtrueを返せば、2.3.2.でログインに成功した旨のレスポンスを返す...という流れのようです。

```php:AuthenticatesUsers.php
        // 2.3. ログイン処理

        // 2.3.1. ログインの認証処理
        if ($this->attemptLogin($request)) {

            // 2.3.2 ログイン成功のレスポンス返却処理
            return $this->sendLoginResponse($request);
        }
```

###2.3.1. ログインの認証処理

`attemptLogin`メソッドではデフォルトに指定したガードの`attempt`メソッドを呼び出しているだけで、その結果を論理値で返します。

```php:AuthenticatesUsers.php
    protected function attemptLogin(Request $request)
    {
        return $this->guard()->attempt(
            // 引数1：credentialsはリクエストから`username`と`password`以外の属性を削除した結果を返す
            // 引数2：filledはフォームの「remember me」チェックボックスにチェックが入っていたかどうかを返す
            $this->credentials($request), $request->filled('remember')
        );
    }
```

`attempt`メソッドは、これまたやっていることが多いので分割して見ます。

```php:SesionGuard.php
  public function attempt(array $credentials = [], $remember = false)
  {
     // 2.3.1.1. ログイン認証のイベント発行処理 
      $this->fireAttemptEvent($credentials, $remember);

      // 2.3.1.2. ユーザ情報の取得処理
      $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

      // 2.3.1.3. 取得したユーザとテーブルに保持するユーザの情報が一致するかのチェック処理
      if ($this->hasValidCredentials($user, $credentials)) {

          // 2.3.1.4. ログイン処理 
          $this->login($user, $remember);

          return true;
      }

      return false;
  }
```

####2.3.1.1. ログイン認証のイベント発行処理 

```php:SesionGuard.php
$this->fireAttemptEvent($credentials, $remember);
```

こちらも、ログイン認証が発生していることのイベントを発行しています。

2.2.2.同様、イベントについては割愛します。

####2.3.1.2. ユーザ情報の取得処理

```php:SesionGuard.php
// 2.3.1.2. ユーザ情報の取得処理
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
```

```php:EloquentUserProvider.php
public function retrieveByCredentials(array $credentials)
{
    // $credentialsが空でないか
    if (empty($credentials) ||
       // $credentials配列の長さが1でないか（=usernameとpasswordのいずれかだけでないか）
       (count($credentials) === 1 &&
       // $credentials配列に'password'キーが含まれているか
        array_key_exists('password', $credentials))) {
        return;
    }

    $query = $this->newModelQuery();

    foreach ($credentials as $key => $value) {
        // $keyが'password'なら次のループへ（=passwordではない方のパラメータでテーブル検索をしたい）
        if (Str::contains($key, 'password')) {
            continue;
        }

        // $credentialに含まれる認証情報（=デフォルトはemail）が、テーブルに存在するか確認する
        if (is_array($value) || $value instanceof Arrayable) {
            $query->whereIn($key, $value);
        } else {
            $query->where($key, $value);
        }
    }

    // 取得したユーザを返す
    return $query->first();
}
```

`retrieveByCredentials`メソッドで、リクエストされたユーザに合致するものがテーブルに存在するかどうか検索します。

（ここでは`password`の検証は行わず、`username()`に基づくユーザの有無だけを検証します）

指定した情報に当てはまるユーザが見つかった場合、`$user`に対象のユーザが設定されます。


###2.3.1.3. 取得したユーザとテーブルに保持するユーザの情報が一致するかのチェック処理

```php:SesionGuard.php
// 2.3.1.3. 取得したユーザとテーブルに保持するユーザの情報が一致するかのチェック処理
      if ($this->hasValidCredentials($user, $credentials)) {
```

```php:SesionGuard.php
protected function hasValidCredentials($user, $credentials)
{
    return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
}
```

取得したユーザとリクエストの認証情報を基に`hasValidCredentials`メソッドを実行します。

`hasValidCredentials`メソッドでは「`$user`がnullでないこと（=2.3.1.2.でユーザ情報が取得できていること）」「`validateCredentials`メソッドの返り値がtrueであること」をを確認しています。

```php:EloquentUserProvider.php
public function validateCredentials(UserContract $user, array $credentials)
{
    // リクエストに含まれる平文のパスワード
    $plain = $credentials['password'];

    // 平文のパスワードをハッシュ化したものと、テーブルに含まれるパスワードが同一かチェックし、合致すればtrueを返す
    return $this->hasher->check($plain, $user->getAuthPassword());
}
```

`validCredentials`メソッドでは平文のパスワードをハッシュ化したものと、テーブルに含まれるパスワードが同一かチェックしています。

この`hasValidCredentials`メソッドの返り値がtrueであれば、ユーザがシステムに存在する＆パスワードが一致するので、ようやく`login`メソッドが呼べます！長かった...。

####2.3.1.4. ログイン処理

```php:AuthenticatesUsers.php
          // 2.3.1.4. ログイン処理 
          $this->login($user, $remember);
```

`login`メソッドは以下の通りです。

```php:SesionGuard.php
public function login(AuthenticatableContract $user, $remember = false)
{
    // セッションにユーザ情報を格納する
    $this->updateSession($user->getAuthIdentifier());

    // 「remember me」のチェックが入っていた場合
    if ($remember) {
        // ランダムな60文字の文字列からなるトークンを作成しUserテーブルのremember_tokenに保存する
        $this->ensureRememberTokenIsSet($user);
        // Cookieに上記と同じトークンを保存する
        $this->queueRecallerCookie($user);
    }

    // ログインイベントを発行する
    $this->fireLoginEvent($user, $remember);

    // userにログインしたユーザ情報をセットする
    $this->setUser($user);
}
```

一番最初の処理でセッションにユーザを識別する情報（デフォルトでは$user->id）を保存しており、これが実際のログインの処理に当たります。「ログインしている状態」とは具体的にはセッションにユーザIDが保存されている状態ということですね。

「remember me」にチェックを入れてログインした場合`if ($remember)`がtrueになるのでUserテーブルのremember_tokenカラムにトークン用に生成された文字列が格納されます。

セッションが切れていてもCookieに含まれるトークンとUserテーブルのremember_tokenが一致すれば同じユーザであることが保証されるので再度のログイン処理が不要になります。

末尾の処理ではsetUserメソッドで`$this`（ここでloginメソッドはSessionGuardクラスのメソッドなので、SessionGuardインスタンス）の`user`にログインに成功したユーザ情報を設定します。

ログイン後に`Auth::user()`などでユーザを簡単に取得できるのはここでセットしているおかげのようですね！

###2.3.2 ログイン成功のレスポンス返却処理

ログイン認証処理が非常に長くなったので現在位置をおさらいします。

```php:AuthenticatesUsers.php
        // 2.3. ログイン処理

        // 2.3.1. ログインの認証処理
        if ($this->attemptLogin($request)) { // <-ログインに成功し、attemptLoginの戻り値がtrueになったところ

            // 2.3.2 ログイン成功のレスポンス返却処理
            return $this->sendLoginResponse($request);
        }
```

2.3.1.でログインに成功し`if($this->attemptLogin($request))`がtrueになったので、ログイン成功のレスポンスを作成する処理が実行されるのでした。

`sendLoginResponse`もやることが多いので、分割して中身を見ていきます。

```php:AuthenticatesUsers.php
    protected function sendLoginResponse(Request $request)
    {
        // セッションを再作成する
        $request->session()->regenerate();

        // ログイン失敗回数をリセットする
        $this->clearLoginAttempts($request);

        // リダイレクトする
        return $this->authenticated($request, $this->guard()->user())
                ?: redirect()->intended($this->redirectPath());
    }
```

`session()->regenerate()`の役割についてはドキュメントより以下の通りです。

>**セッションIDの再生成**

>セッションIDの再生成は多くの場合、悪意のあるユーザーからの、アプリケーションに対するsession fixation攻撃を防ぐために行います。

>Laravelに組み込まれているLoginControllerを使用していれば、認証中にセッションIDは自動的に再生成されます。しかし、セッションIDを任意に再生成する必要があるのでしたら、regenerateメソッドを使ってください。

参考：[Laravel 6.x HTTPセッション](https://readouble.com/laravel/6.x/ja/session.html)

`clearLoginAttempts`ではログインに成功したので、加算されていたログイン失敗回数をリセットします。

`redirectPath`メソッドでは`redirectTo`変数が設定されているかをチェックし、設定されている場合は`$redirectTo`で指定したパスに、設定されていない場合は`/home`にリダイレクトされます。

```php:AuthenticatesUsers.php
    public function redirectPath()
    {
        if (method_exists($this, 'redirectTo')) {
            return $this->redirectTo();
        }

        return property_exists($this, 'redirectTo') ? $this->redirectTo : '/home';
    }
```


`redirectTo`には`LoginController`内で、ホーム画面へのパスが指定されています。

ログイン後に遷移する画面を変更したい場合、`redirectTo`のパス指定を変更するだけ簡単に実現できます。

```php：LoginController.php
// デフォルトではhome
protected $redirectTo = RouteServiceProvider::HOME;
```

:::note warn
ログインに成功すればログイン成功のレスポンスがreturnされるため、コントローラの処理はここで終了です。
下記の2.4.以降の処理はログインに失敗した場合に行われる処理になります。
:::

##2.4. ログイン失敗回数を加算する処理

returnされずにこの処理に到達したということはログインに失敗しているので、ログイン失敗回数を加算します。

```php:AuthenticatesUsers.php
        // 2.4. ログイン失敗回数を加算する処理
        $this->incrementLoginAttempts($request);
```

`incrementLoginAttempts`メソッドでは、ログイン失敗回数を保持するキーと再度ログインを試行できるようになるまでの時間（デフォルトでは60秒）を引数に`hit`メソッドを呼び出します。

```php:ThrottlesLogins.php
    protected function incrementLoginAttempts(Request $request)
    {
        $this->limiter()->hit(
            $this->throttleKey($request), $this->decayMinutes() * 60
        );
    }
```

`hit`メソッドでは先に登場した通り、キャッシュに`:timer`をセットします。

この`:timer`がキャッシュ上に残っている限り、規定回数パスワードを誤った後の再度ログインを試行できないというやつですね。

```php:RateLimiter.php
    public function hit($key, $decaySeconds = 60)
    {
        // キャッシュに':timer'をセットする
        $this->cache->add(
            $key.':timer', $this->availableAt($decaySeconds), $decaySeconds
        );

        // ログイン失敗回数「0」をキャッシュに保存する
        $added = $this->cache->add($key, 0, $decaySeconds);

        // キャッシュに保存されたログイン失敗回数に+1する
        $hits = (int) $this->cache->increment($key);

        // $addedがfalseである（=既にキャッシュにキーが存在したため、今回が初めてのログイン失敗ではない）
        // かつ、hitsの値が１である場合
        if (! $added && $hits == 1) {
            // ログイン失敗回数「1」をキャッシュに保存する
            $this->cache->put($key, 1, $decaySeconds);
        }

        return $hits;
    }
```

`cache->add`ではキャッシュに新たなキーと値の組を保存しますが、保存できたかどうかがわかるように保存した場合`true`、できなかった場合`false`を返してくれます。

つまり既にキーが存在する場合（=すでにログインに失敗しており、ログイン失敗回数が保持されている場合）はキーを作成せずにfalseが返され、まだキーが存在しない場合（=初めてのログイン失敗）は新たにログイン失敗回数として「0」をキャッシュに保存しtrueを返します。

また、`add`メソッドは第三引数としてそのキャッシュが消滅するまでの時間を指定できるので、ログインの失敗は`$decaySeconds`が経過すれば無かったことになるわけですね。

そのまま下の`increment`で失敗回数を増やしているので、キーが存在すればそのまま今までの失敗回数に1が加算され、存在しなければ新たに失敗回数として１が記録されます。

`if (! $added && $hits == 1)`は「**すでに失敗回数のキーが存在した**かつ**$hitsの値が1である**（=incrementの結果、失敗回数が「1」になった）場合にログイン失敗回数に「1」を設定するというものです...が、どういったケースの時にこの条件に当てはまるのかが怪しいです...。

この`hit`メソッド外で既に失敗回数「0」のキャッシュが作成されていた場合でしょうか？


##2.5. ログイン失敗のレスポンスを返す処理

```php:AuthenticatesUsers.php
        // 2.5. ログイン失敗のレスポンスを返す処理
        return $this->sendFailedLoginResponse($request);
```

`sendFailedLoginResponse`メソッドで作成されたレスポンスが返されます。

```php:AuthenticatesUsers.php
    protected function sendFailedLoginResponse(Request $request)
    {
        throw ValidationException::withMessages([
            $this->username() => [trans('auth.failed')],
        ]);
    }
```

`sendFailedLoginResponse`メソッドで、2.2.2.のログイン失敗回数超過の場合と同様にバリデーションの例外を投げます。

エラーメッセージは`username`に対して`resources\lang\en\auth.php`に記載された`failed`が設定されます。

```php:auth.php
    // 入力された認証情報に一致するレコードがありません
    'failed' => 'These credentials do not match our records.',
```

こちらも`failed`メッセージの内容を書き換えれば、好きなログイン失敗メッセージを設定できますね。

#まとめ

ログイン処理を細かく追いかけることで、「ログイン処理で何が起きているか」「何をもってログイン状態とするか」について理解することができました。

どこで何を行ってログイン処理が成り立っているかが分かったので、特定の機能をカスタマイズするときに実装する必要がある箇所の目星がつけやすくなりました。

ログインと同時に自動生成されるパスワードリセット処理についても、時間を作ってそちらも追いかけてみたいと思います:v: