【Laravel】ログイン失敗時のロック機能が正常動作しない場合の対応

# 結論
プロキシサーバを経由してクライアントIPを取得する際は
`Symfony\Component\HttpFoundation`クラスの`setTrustedProxies()`を利用しよう。

# 事の発端
- とある案件で、Laravelの標準機能を使って`ログインに失敗した場合のロック機能`を実装したが、「ローカル環境ではちゃんと動作するのにも関わらず、テスト環境(GCP)では正常に動作しない」という事態があった
- 具体的には、テスト環境(GCP)で動作しているログイン機能では「規定回数でロックがかかるはずなのに、**規定回数を超えたにも関わらずログインを試行できる状態**になったり、**いつまでたってもロックがかからない**」といった状況

# Laravelの標準の「ログインに失敗した場合のロック機能」について
まずは前提知識として、Laravelのログインロック機能について触れておく

- Laravel標準の`LoginController`を利用してログイン認証を実装する場合、 既にデフォルトで「ログイン失敗の数によってログイン制限をかける」といった機能が用意されている
※ デフォルトは試行回数：5回／ロック時間：1分

- 試行回数やロック時間などのデフォルトの設定を変更したい場合は、以下のようにLoginControllerに「ログイン試行回数」と「ロックする時間(分)」を記載することでカスタマイズが可能

```php:app/Http/Controllers/Auth/LoginController.php
class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

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

`LoginController`は`AuthenticatesUsers`トレイトをuseしているが、
ログイン失敗時のロック機能は、この`AuthenticatesUsers`トレイトでuseされている`ThrottlesLogins`トレイト内で実装されている。

この`ThrottlesLogins`トレイトの中身を見てみると、ログインの試行回数をチェックするメソッドは以下の実装になっている。

```php:vendor/laravel/framework/src/Illuminate/Foundation/Auth/ThrottlesLogins.php
    /**
     * Determine if the user has too many failed login attempts.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    protected function hasTooManyLoginAttempts(Request $request)
    {
        return $this->limiter()->tooManyAttempts(
            $this->throttleKey($request), $this->maxAttempts()
        );
    }
```

ここで呼ばれている`throttleKey()`メソッドは、以下の実装となっている。

```php:vendor/laravel/framework/src/Illuminate/Foundation/Auth/ThrottlesLogins.php
    /**
     * Get the throttle key for the given request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string
     */
    protected function throttleKey(Request $request)
    {
        return Str::lower($request->input($this->username())).'|'.$request->ip();
    }
```
どうやら、ロック対象の判定には「リクエストで受け取った`username`と`ip`」を利用しているようだ。


# 原因調査
まず、問題のテスト環境(GCP)で、上記のメソッドがどのような値を返却するのかログに吐き出してみた。

```php:app/Http/Controllers/Auth/LoginController.php
    public function authorize(Request $request) {
        Log::debug('throttleKey -> '.print_r($this->throttleKey($request), 1));
    }
```

結果は、以下のような形でログ出力される。

```
[2019-06-24 12:07:32] local.DEBUG: throttleKey -> mail_address@hoge.com|111.222.333.444  
```

なんと、IPアドレスの最後の444の部分(ホスト部)が、アクセスする度に動的に変化していることがわかった。

どうやら事象の原因は「アクセスする度に異なるIPのプロキシサーバを経由し、そのプロキシのIPアドレスを取得してしまっていることで、**ロックすべき対象が判定が出来ていないため**」であるようだ。

そこで、`throttleKey()`メソッドの中で、IPアドレスを取得しているロジックを追いかけると、継承元のSymfonyクラスにたどり着く。

```php:vendor/symfony/http-foundation/Request.php
    /**
     * Returns the client IP address.
     *
     * This method can read the client IP address from the "X-Forwarded-For" header
     * when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For"
     * header value is a comma+space separated list of IP addresses, the left-most
     * being the original client, and each successive proxy that passed the request
     * adding the IP address where it received the request from.
     *
     * @return string|null The client IP address
     *
     * @see getClientIps()
     * @see http://en.wikipedia.org/wiki/X-Forwarded-For
     */
    public function getClientIp()
    {
        $ipAddresses = $this->getClientIps();

        return $ipAddresses[0];
    }
```

親切なことに、解決方法がDocコメントに書いてある。
`setTrustedProxies()`メソッドを利用することで、「X_FORWARDED_FOR」ヘッダからクライアントIPを取得できるとのこと。

# プロキシサーバを経由した場合に、クライアントIPを取得できる設定を追加する

LoginControllerに以下の設定を追記

```php:app/Http/Controllers/Auth/LoginController.php
    public function authorize(Request $request) {
        // プロキシサーバを経由した場合に、X-Forwarded-Forヘッダから接続元のクライアントIPを取得する設定
        $request::setTrustedProxies($request->ip(), $request::getTrustedHeaderSet());
    }
```

無事にテスト環境(GCP)でもクライアントIPを判定することができ、
ログイン失敗時のロック機能が正常に動作することを確認できた。

---

参考記事
[【Laravel5】X_FORWARDED_FOR の取得方法 - Qiita](https://qiita.com/sh-watanabe/items/c3695bcecccd00fd91dd)