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 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 1

为啥用两个redis的key...一个key不就解决了

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!