RateLimiter
RateLimiter
我们在平常开发中,经常遇到这样的需求,例如如果用户在一分钟以内登陆失败超过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 头中,返回给客户端。
本作品采用《CC 协议》,转载必须注明作者和本文链接
为啥用两个redis的key...一个key不就解决了