結論
プロキシサーバを経由してクライアントIPを取得する際は
Symfony\Component\HttpFoundation
クラスのsetTrustedProxies()
を利用しよう。
事の発端
- とある案件で、Laravelの標準機能を使って
ログインに失敗した場合のロック機能
を実装したが、「ローカル環境ではちゃんと動作するのにも関わらず、テスト環境(GCP)では正常に動作しない」という事態があった - 具体的には、テスト環境(GCP)で動作しているログイン機能では「規定回数でロックがかかるはずなのに、規定回数を超えたにも関わらずログインを試行できる状態になったり、いつまでたってもロックがかからない」といった状況
Laravelの標準の「ログインに失敗した場合のロック機能」について
まずは前提知識として、Laravelのログインロック機能について触れておく
-
Laravel標準の
LoginController
を利用してログイン認証を実装する場合、 既にデフォルトで「ログイン失敗の数によってログイン制限をかける」といった機能が用意されている
※ デフォルトは試行回数:5回/ロック時間:1分 -
試行回数やロック時間などのデフォルトの設定を変更したい場合は、以下のようにLoginControllerに「ログイン試行回数」と「ロックする時間(分)」を記載することでカスタマイズが可能
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
トレイトの中身を見てみると、ログインの試行回数をチェックするメソッドは以下の実装になっている。
/**
* 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()
メソッドは、以下の実装となっている。
/**
* 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)で、上記のメソッドがどのような値を返却するのかログに吐き出してみた。
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クラスにたどり着く。
/**
* 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に以下の設定を追記
public function authorize(Request $request) {
// プロキシサーバを経由した場合に、X-Forwarded-Forヘッダから接続元のクライアントIPを取得する設定
$request::setTrustedProxies($request->ip(), $request::getTrustedHeaderSet());
}
無事にテスト環境(GCP)でもクライアントIPを判定することができ、
ログイン失敗時のロック機能が正常に動作することを確認できた。