Laravelって便利なもので、LoginControllerにプロパティを設定するだけでアカウントロック機能を実装できちゃいます。
(LoginControllerに設定しなくてもデフォルトで、5回のログイン失敗を許容し、6回失敗したら1分間ロックされるようになっています)
// 下記の場合だと、10回失敗OK、11回失敗したら5分間アカウントがロックされます
protected $maxAttempts = 10;
protected $decayMinutes = 5;
この記事は、そんなLaravelでデフォルトで設定されているアカウントロックをせずに、別の処理を出来るようにカスタマイズしちゃおう!というものです。
↓バージョンなど
Laravel >= 5.6
PHP >= 7.1
そもそもアカウントロックってどこで設定されてる?
どう処理が走ってアカウントロックが起こるのか、まず確認してみます。
ログイン処理の実装周りは、Illuminate/Foundation/Auth/AuthenticatesUsers.php
に書かれています。
public function login(Request $request)
{
$this->validateLogin($request);
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);
}
// If the login attempt was unsuccessful we will increment the number of attempts
// to login and redirect the user back to the login form. Of course, when this
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
これを見ると、sendLockoutResponse
がアカウントロックの処理をしてそうだと推測できます。
ということでsendLockoutResponse
がどうなっているのか確認します。
このメソッドはコメントアウトにもあるように、AuthenticatesUsers.php
で使用しているThrottlesLogins
トレイトに書かれています。
protected function sendLockoutResponse(Request $request)
{
$seconds = $this->limiter()->availableIn(
$this->throttleKey($request)
);
throw ValidationException::withMessages([
$this->username() => [Lang::get('auth.throttle', ['seconds' => $seconds])],
])->status(429);
}
コードの内容を見ると、やはりここでアカウントロックをかけているようです!(ValidationExceptionが投げられている)
では次にこのsendLockoutResponse
メソッドが実行される条件であるhasTooManyLoginAttempts
メソッドを確認してみましょう。
protected function hasTooManyLoginAttempts(Request $request)
{
return $this->limiter()->tooManyAttempts(
$this->throttleKey($request), $this->maxAttempts()
);
}
うーん、これだけでは分からないですね。。。
tooManyAttempts
の処理如何のようです。ちなみに、その引数に渡されている$this->throttleKey($request)
と$this->maxAttempts()
はそれぞれ「ユーザー名(email)+ IP」と「ログイン試行可能回数」を返します。
ではtooManyAttempts
を確認していきますが、これはRateLimiter
クラスに書かれています。($this->limiter()
はそれをインスタンス化するメソッド)
public function tooManyAttempts($key, $maxAttempts)
{
if ($this->attempts($key) >= $maxAttempts) {
if ($this->cache->has($key.':timer')) {
return true;
}
$this->resetAttempts($key);
}
return false;
}
$this->attempts($key)
は同じクラス内に書かれているのですが、各ユーザーがキャッシュとして保持しているログイン試行回数を返します。
なのでこのコードを見ると、プロパティに設定されたログイン試行可能回数以上にログインを試みて、且つ$key.':timer'
をキーとするキャッシュを保持している場合のみ、sendLockoutResponse
メソッドは実行されるようです。
では、$key.':timer'
をキーとして値を保存させている処理はどうなっているのでしょう?
それは、同じクラス内にありました。
public function hit($key, $decayMinutes = 1)
{
$this->cache->add(
$key.':timer', $this->availableAt($decayMinutes * 60), $decayMinutes
);
$added = $this->cache->add($key, 0, $decayMinutes);
$hits = (int) $this->cache->increment($key);
if (! $added && $hits == 1) {
$this->cache->put($key, 1, $decayMinutes);
}
return $hits;
}
$this->cache->add
で設定してますね。ログイン試行回数もここで保存してます。
ただここで問題が起こる事が分かります。それはこのキャッシュの保持時間が$decayMinutes
であること。
何か嫌な予感がしますね。。。
ということでこのメソッドがThrottlesLogins
トレイトでどう使われているか確認してみましょう。
protected function incrementLoginAttempts(Request $request)
{
$this->limiter()->hit(
$this->throttleKey($request), $this->decayMinutes()
);
}
嫌な予感的中!
$this->decayMinutes()
は$decayMinutes
プロパティを返すメソッドです。
つまり、$decayMinutes
を0に設定してしまうと、「アカウントロックせずに別の処理を走らせる」という事が出来ません。。。
(hasTooManyLoginAttempts
メソッドがtrueを返すことが無いので)
アカウントロックせずに別の処理を走らせたい!
では本題。
上記の調査で、$decayMinutes
を0に設定してしまうと、アカウントロックせずに別の処理を走らせられない事が分かりました。
なので、hasTooManyLoginAttempts
メソッドをLoginController内でオーバーライドして、一定の条件下ではtrueを返すようにしましょう!
hasTooManyLoginAttempts
の中身は実質tooManyAttempts
メソッドであるので、これを参考にコードを書いていきます。
protected function hasTooManyLoginAttempts(Request $request)
{
$attemptCount = \Cache::get($this->throttleKey($request));
if ($attemptCount >= $this->maxAttempts()) {
$credentials = $request->only('email', 'password');
if (! \Auth::attempt($credentials)) {
return true;
}
\Cache::forget($this->throttleKey($request));
}
return false;
}
tooManyAttempts
と異なる点は、ネストされたif文の条件式です。
Authファサードのattempt
メソッドを利用して、認証できればfalse、出来なければtrueが返るようにします。
こうする事でsendLockoutResponse
メソッドを実行できるようになります!
最後に、sendLockoutResponse
もオーバーライドしておきましょう。そうしないと、ログイン試行可能回数を超えた時に、「0 - 現在時刻」待たなければいけません。
「-1566617015秒お待ちください」とか表示されます!笑
protected function sendLockoutResponse(Request $request)
{
// 何かしたい処理を書く。
}
いやー、Laravelのvendorディレクトリ以下を見ていくのってちょっと億劫だったのですが、今回の学びで少し勇気が出ました。