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 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 1

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

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
2
粉丝
0
喜欢
5
收藏
9
排名:1318
访问:3092
私信
所有博文
社区赞助商