laavel5.5 throttle middleware源码解析

场景

  • 经常会用到throttle, 那么throttle是怎么实现的呢?

参考资料

源码分析

  • 位置
    • 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
  • 原理
    • 利用CacheServiceProvider实现了一个简单计数器
  • 追踪源码
    • $key = $this->resolveRequestSignature($request);
      • 为请求生成唯一签名
      • resolveRequestSignature源码
        • 当用户登陆的时 ,签名是用户ID sha1散列值
        • 当用户未登陆时, 签名是sha1($route->getDomain().'|'.$request->ip())
      • $maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts);
        • 获取当前throttle允许请求的最大次数
        • resolveMaxAttempts源码 可以看到throttle 支持middleware(‘throttle:40|10,1’) 格式配置, 当用户登陆的时候 限定是10, 否则时40
      • $this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)
        • 判断是否已经超过了访问次数限制
        • tooManyAttempts源码 从CacheServiceProvider中取出签名 k e y 访 key的值,并且作为已经访问过的次数和上一步获取的 maxAttempts做进一步的比较, 如果已访问次数达到访问允许的上限 && 此时计时器没有消失 则抛出Too Many Requests的异常。如果此时计时器消失 则清除计数器。并继续下面的执行。如果没有达到允许访问的上线 则继续执行。
      • $this->limiter->hit($key, $decayMinutes);
        • create 计时器 计数器 或者update 计数器
        • hit源码分析
          • 如果计时器不存在 则create计时器 , 键是 k e y . : t i m e r key.':timer',存活周期 decayMinutes,
          • 如果不存在计数器,则create计数器 键是 k e y key 周期是 decayMinutes, 默认值0
          • increment计数器
      • $response = $next($request);
        • 继续下面的请求
总结
  • 通过源码分析可以看到 一个用户的请求可以生成唯二的签名(登陆 &&未登陆)
  • throttle针对的是一个用户或者是一个IP在一定时间内的请求总量,并没有针对到特定的路由, 如果有需要的话 可以修改resolveRequestSignature, sha1参数可以加上$request->route()->getActionName()
  • throttle 设置周期应该保持一致,否则以第一次为准
    • 路由test ‘throttle:40|10,1’, 路由 test2 ‘throttle:30|5,2’, 在第60秒,计时器消失 test正常, 但是test2也刷新了计时器 计数器,并没有达到预期的目的
hit源码
    /**
     * Increment the counter for a given key for a given decay time.
     *
     * @param  string  $key
     * @param  float|int  $decayMinutes
     * @return int
     */
    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;
    }
tooManyAttempts
    /**
     * Determine if the given key has been "accessed" too many times.
     *
     * @param  string  $key
     * @param  int  $maxAttempts
     * @param  float|int  $decayMinutes
     * @return bool
     */
    public function tooManyAttempts($key, $maxAttempts, $decayMinutes = 1)
    {
        if ($this->attempts($key) >= $maxAttempts) {
            if ($this->cache->has($key.':timer')) {
                return true;
            }

            $this->resetAttempts($key);
        }

        return false;
    }
    /**
     * Get the number of attempts for the given key.
     *
     * @param  string  $key
     * @return mixed
     */
    public function attempts($key)
    {
        return $this->cache->get($key, 0);
    }
    /**
     * Reset the number of attempts for the given key.
     *
     * @param  string  $key
     * @return mixed
     */
    public function resetAttempts($key)
    {
        return $this->cache->forget($key);
    }
resolveMaxAttempts

    /**
     * Resolve the number of attempts if the user is authenticated or not.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int|string  $maxAttempts
     * @return int
     */
    protected function resolveMaxAttempts($request, $maxAttempts)
    {
        if (Str::contains($maxAttempts, '|')) {
            $maxAttempts = explode('|', $maxAttempts, 2)[$request->user() ? 1 : 0];
        }

        return (int) $maxAttempts;
    }
throttle源码
  /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  int|string  $maxAttempts
     * @param  float|int  $decayMinutes
     * @return mixed
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
     */
    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, $decayMinutes)) {
            throw $this->buildException($key, $maxAttempts);
        }

        $this->limiter->hit($key, $decayMinutes);

        $response = $next($request);

        return $this->addHeaders(
            $response, $maxAttempts,
            $this->calculateRemainingAttempts($key, $maxAttempts)
        );
    }
为请求生成唯一签名
    /**
     * Resolve request signature.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string
     * @throws \RuntimeException
     */
    protected function resolveRequestSignature($request)
    {
        if ($user = $request->user()) {
            return sha1($user->getAuthIdentifier());
        }

        if ($route = $request->route()) {
            return sha1($route->getDomain().'|'.$request->ip());
        }

        throw new RuntimeException(
            'Unable to generate the request signature. Route unavailable.'
        );
    }

猜你喜欢

转载自blog.csdn.net/cominglately/article/details/86617292