我們在平常開發中,經常遇到這樣的需求,例如如果使用者在一分鐘以內登陸失敗超過5次,則需要等一分鐘在重試;限制每個手機號每分鐘只能傳送一次簡訊驗證碼;api介面限速,每個ip每分鐘最多60次。對這樣的需求進行抽象,就是限制在一段時間內做某件事情的次數,避免伺服器遭受輪詢的破解。
在 Laravel 中,針對這種需求,抽象了一個類: RateLimiter。這個類就是用來解決這樣的問題的。
依賴
RateLimiter 的程式碼是在框架的 Cache 目錄下的,同時 RateLimiter 也依賴於 Cache 元件。
在 Laravel 中,Cache 有多種配置,例如 檔案快取、資料庫快取、apc、memcached、redis等等。如果應用部署在一臺伺服器上,那麼這些快取在使用上沒有什麼區別;如果部署在多臺伺服器上,那麼 RateLimiter 就會出現問題。
因此,在使用 RateLimiter 的時候,建議使用資料庫、redis、memcached 這樣的快取配置。
原理
我們以限流需求舉例,假設60秒內限制5次。當第一個請求到來的時候,我們需要初始化計數器為1,同時給計數器設定超時時間60秒。再來一個請求的時候,我們就把計數器加一,當第6個請求到來的時候,計時器已經是5了,超過了配置的5次請求,就會拒絕,並告訴客戶端超過了訪問次數。當60秒過去之後,計數器超時被刪除,這樣就可以重新接收新的請求。
方法
http://naotu.baidu.com/file/967c4de22f54ce...
登陸
Auth 元件的登陸介面使用了 RateLimiter,接下來我們看下在Auth元件中,是怎麼使用 RateLimiter 的。
首先,我們看下 login 的原始碼:
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);
}
在其中 hasTooManyLoginAttempts
方法裡面,呼叫了 RateLimiter 的 tooManyAttempts
方法,用於校驗是否超過了配置的次數。
protected function hasTooManyLoginAttempts(Request $request)
{
return $this->limiter()->tooManyAttempts(
$this->throttleKey($request), $this->maxAttempts()
);
}
如果登陸超過了配置的次數,那麼就會從 RateLimiter 中下次能呼叫登陸介面所需要多少秒
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);
}
如果登陸成功,在 sendLoginResponse
方法中,會呼叫 clear
方法,清空 RateLimter
protected function sendLoginResponse(Request $request)
{
$request->session()->regenerate();
$this->clearLoginAttempts($request);
return $this->authenticated($request, $this->guard()->user())
?: redirect()->intended($this->redirectPath());
}
protected function clearLoginAttempts(Request $request)
{
$this->limiter()->clear($this->throttleKey($request));
}
如果登陸失敗,會呼叫 hit
方法,讓計數器+1
protected function incrementLoginAttempts(Request $request)
{
$this->limiter()->hit(
$this->throttleKey($request), $this->decayMinutes()
);
}
上面就是 RateLimiter
在登陸過程中的例子,總結下使用了那些方法
1. 首先,呼叫 tooManyAttempts 判斷計數器是否超過了最大次數
2. 如果超過了最大次數,就呼叫 availableIn 方法,獲取下次可以訪問還需要多少秒
3. 如果登陸成功,呼叫 clear 方法清空計數器
4. 如果登陸失敗,呼叫 hit 方法,讓計數器+1
限流中介軟體
ThrottleRequests
是 laravel 提供的一個限流中介軟體,我們來看下它的原始碼,看看 ThrottleRequests
是怎麼使用 RateLimiter
。
首先,先看下中介軟體的 Handler 方法:
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
$maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts);
if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
throw $this->buildException($key, $maxAttempts);
}
$this->limiter->hit($key, $decayMinutes);
$response = $next($request);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
可以看到,handler 方法中首先呼叫了 tooManyAttempts
方法校驗是否已經達到最大次數,如果達到最大次數,那麼在 buildException
也會呼叫 availableIn
方法獲取下次請求的所需要多少秒:
protected function buildException($key, $maxAttempts)
{
$retryAfter = $this->getTimeUntilNextRetry($key);
$headers = $this->getHeaders(
$maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
return new ThrottleRequestsException(
'Too Many Attempts.', null, $headers
);
}
protected function getTimeUntilNextRetry($key)
{
return $this->limiter->availableIn($key);
}
如果沒有超過最大次數呢,就呼叫 hit 方法將計數器+1。
在請求結束之後($next
閉包執行完成之後),也會呼叫 retriesLeft
獲取在一分鐘之內,剩餘多少次請求。
protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
{
if (is_null($retryAfter)) {
return $this->limiter->retriesLeft($key, $maxAttempts);
}
return 0;
}
接下來總結下在 ThrottleRequests
方法中,是如何使用 RateLimiter
的。
1. 首先呼叫 tooManyAttempts 判斷計數器是否超過了最大次數
2. 如果超過了最大次數,需要呼叫 availableIn 方法,獲取下次可以訪問還需要多少秒,同時將這個值放在 header 頭中,返回給客戶端
3. 如果沒有超過最大次數,呼叫 retriesLeft 方法,獲取在一個計數週期內,還剩下多少次請求,同時也將這個值放在 header 頭中,返回給客戶端。