lumen自定义封装api限流中间件

发布时间:2024年01月23日

背景

现在公司重构api项目,针对有些写入和请求的接口需要进行限制设置。比如说一分钟60次等。看了网上的都是laravel的throttle限流,但是没有针对lumen的,所以需要自己重新封装。

实现

  • 1.在App\Http\Middleware下创建一个自定义的中间件,名为ThrottleRequestsMiddleware。
    在这里插入图片描述
  • 2 将如下自定义的中间件代码复制粘贴到中间件内。
    备注:这里代码已经封装改写好了,复制粘贴既可以用
<?php

namespace App\Http\Middleware;

use App\Exceptions\ThrottleException;
use Closure;
use Illuminate\Cache\RateLimiter;
use Illuminate\Cache\RateLimiting\Unlimited;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Support\Arr;
use Illuminate\Support\InteractsWithTime;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;

class ThrottleRequestsMiddleware
{
   use InteractsWithTime;

   /**
    * The rate limiter instance.
    *
    * @var \Illuminate\Cache\RateLimiter
    */
   protected $limiter;

   /**
    * Indicates if the rate limiter keys should be hashed.
    *
    * @var bool
    */
   protected static $shouldHashKeys = true;

   /**
    * Create a new request throttler.
    *
    * @param  \Illuminate\Cache\RateLimiter  $limiter
    * @return void
    */
   public function __construct(RateLimiter $limiter)
   {
       $this->limiter = $limiter;
   }

   /**
    * Specify the named rate limiter to use for the middleware.
    *
    * @param  string  $name
    * @return string
    */
   public static function using($name)
   {
       return static::class.':'.$name;
   }

   /**
    * Specify the rate limiter configuration for the middleware.
    *
    * @param  int  $maxAttempts
    * @param  int  $decayMinutes
    * @param  string  $prefix
    * @return string
    *
    * @named-arguments-supported
    */
   public static function with($maxAttempts = 60, $decayMinutes = 1, $prefix = '')
   {
       return static::class.':'.implode(',', func_get_args());
   }

   /**
    * Handle an incoming request.
    *
    * @param  \Illuminate\Http\Request  $request
    * @param  \Closure  $next
    * @param  int|string  $maxAttempts
    * @param  float|int  $decayMinutes
    * @param  string  $prefix
    * @return \Symfony\Component\HttpFoundation\Response
    *
    * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
    */
   public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '')
   {
       if (is_string($maxAttempts)
           && func_num_args() === 3
           && ! is_null($limiter = $this->limiter->limiter($maxAttempts))) {
           return $this->handleRequestUsingNamedLimiter($request, $next, $maxAttempts, $limiter);
       }

       return $this->handleRequest(
           $request,
           $next,
           [
               (object) [
                   'key' => $prefix.$this->resolveRequestSignature($request),
                   'maxAttempts' => $this->resolveMaxAttempts($request, $maxAttempts),
                   'decaySeconds' => 60 * $decayMinutes,
                   'responseCallback' => null,
               ],
           ]
       );
   }

   /**
    * Handle an incoming request.
    *
    * @param  \Illuminate\Http\Request  $request
    * @param  \Closure  $next
    * @param  string  $limiterName
    * @param  \Closure  $limiter
    * @return \Symfony\Component\HttpFoundation\Response
    *
    * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
    */
   protected function handleRequestUsingNamedLimiter($request, Closure $next, $limiterName, Closure $limiter)
   {
       $limiterResponse = $limiter($request);

       if ($limiterResponse instanceof Response) {
           return $limiterResponse;
       } elseif ($limiterResponse instanceof Unlimited) {
           return $next($request);
       }

       return $this->handleRequest(
           $request,
           $next,
           collect(Arr::wrap($limiterResponse))->map(function ($limit) use ($limiterName) {
               return (object) [
                   'key' => self::$shouldHashKeys ? md5($limiterName.$limit->key) : $limiterName.':'.$limit->key,
                   'maxAttempts' => $limit->maxAttempts,
                   'decaySeconds' => $limit->decaySeconds,
                   'responseCallback' => $limit->responseCallback,
               ];
           })->all()
       );
   }

   /**
    * Handle an incoming request.
    *
    * @param  \Illuminate\Http\Request  $request
    * @param  \Closure  $next
    * @param  array  $limits
    * @return \Symfony\Component\HttpFoundation\Response
    *
    * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
    */
   protected function handleRequest($request, Closure $next, array $limits)
   {
       foreach ($limits as $limit) {
           if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
               throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
           }

           $this->limiter->hit($limit->key, $limit->decaySeconds);
       }

       $response = $next($request);

       foreach ($limits as $limit) {
           $response = $this->addHeaders(
               $response,
               $limit->maxAttempts,
               $this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
           );
       }

       return $response;
   }

   /**
    * 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];
       }

       if (! is_numeric($maxAttempts) && $request->user()) {
           $maxAttempts = $request->user()->{$maxAttempts};
       }

       return (int) $maxAttempts;
   }

   /**
    * Resolve request signature.
    *
    * @param  \Illuminate\Http\Request  $request
    * @return string
    *
    * @throws \RuntimeException
    */
   protected function resolveRequestSignature($request)
   {
       return sha1(
           $request->method() .
           '|' . $request->server('SERVER_NAME') .
           '|' . $request->path() .
           '|' . $request->ip()
       );

   }

   /**
    * Create a 'too many attempts' exception.
    *
    * @param  \Illuminate\Http\Request  $request
    * @param  string  $key
    * @param  int  $maxAttempts
    * @param  callable|null  $responseCallback
    * @return \Illuminate\Http\Exceptions\ThrottleRequestsException|\Illuminate\Http\Exceptions\HttpResponseException
    */
   protected function buildException($request, $key, $maxAttempts, $responseCallback = null)
   {
       $retryAfter = $this->getTimeUntilNextRetry($key);

       $headers = $this->getHeaders(
           $maxAttempts,
           $this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
           $retryAfter
       );

       return new ThrottleException('Too Many Attempts.', 429);
   }

   /**
    * Get the number of seconds until the next retry.
    *
    * @param  string  $key
    * @return int
    */
   protected function getTimeUntilNextRetry($key)
   {
       return $this->limiter->availableIn($key);
   }

   /**
    * Add the limit header information to the given response.
    *
    * @param  \Symfony\Component\HttpFoundation\Response  $response
    * @param  int  $maxAttempts
    * @param  int  $remainingAttempts
    * @param  int|null  $retryAfter
    * @return \Symfony\Component\HttpFoundation\Response
    */
   protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
   {
       $response->headers->add(
           $this->getHeaders($maxAttempts, $remainingAttempts, $retryAfter, $response)
       );

       return $response;
   }

   /**
    * Get the limit headers information.
    *
    * @param  int  $maxAttempts
    * @param  int  $remainingAttempts
    * @param  int|null  $retryAfter
    * @param  \Symfony\Component\HttpFoundation\Response|null  $response
    * @return array
    */
   protected function getHeaders($maxAttempts,
                                 $remainingAttempts,
                                 $retryAfter = null,
                                 Response $response = null)
   {
       if ($response &&
           ! is_null($response->headers->get('X-RateLimit-Remaining')) &&
           (int) $response->headers->get('X-RateLimit-Remaining') <= (int) $remainingAttempts) {
           return [];
       }

       $headers = [
           'X-RateLimit-Limit' => $maxAttempts,
           'X-RateLimit-Remaining' => $remainingAttempts,
       ];

       if (! is_null($retryAfter)) {
           $headers['Retry-After'] = $retryAfter;
           $headers['X-RateLimit-Reset'] = $this->availableAt($retryAfter);
       }

       return $headers;
   }

   /**
    * Calculate the number of remaining attempts.
    *
    * @param  string  $key
    * @param  int  $maxAttempts
    * @param  int|null  $retryAfter
    * @return int
    */
   protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
   {
       return is_null($retryAfter) ? $this->limiter->retriesLeft($key, $maxAttempts) : 0;
   }

   /**
    * Format the given identifier based on the configured hashing settings.
    *
    * @param  string  $value
    * @return string
    */
   private function formatIdentifier($value)
   {
       return self::$shouldHashKeys ? sha1($value) : $value;
   }

   /**
    * Specify whether rate limiter keys should be hashed.
    *
    * @param  bool  $shouldHashKeys
    * @return void
    */
   public static function shouldHashKeys(bool $shouldHashKeys)
   {
       self::$shouldHashKeys = $shouldHashKeys ?? true;
   }
}

throttle超过限制时抛出的是Illuminate\Http\Exceptions\ThrottleRequestsException,因为Lumen框架缺少这个文件,需要自己定义一下,在app/Exceptions中新建ThrottleException.php,写入以下代码:
具体位置:
在这里插入图片描述
具体代码:

<?php


namespace App\Exceptions;

use Exception;

class ThrottleException extends Exception
{

    protected $isReport = false;

    public function isReport(){
        return $this->isReport;
    }
}

以上代码复制粘贴即可用。

  • 3 同时需要在app/Exceptions/Handler.php捕获该抛出异常,在render方法增加以下判断:
    在这里插入图片描述

代码如下:

<?php

namespace App\Exceptions;

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that should not be reported.
     *
     * @var array
     */
    protected $dontReport = [
        AuthorizationException::class,
        HttpException::class,
        ModelNotFoundException::class,
        ValidationException::class,
    ];

    /**
     * Report or log an exception.
     *
     * This is a great spot to send exceptions to Sentry, Bugsnag, etc.
     *
     * @param  \Throwable  $exception
     * @return void
     *
     * @throws \Exception
     */
    public function report(Throwable $exception)
    {
        parent::report($exception);
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Throwable  $exception
     * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
     *
     * @throws \Throwable
     */
    public function render($request, Throwable $exception)
    {
    	//此处增加异常捕捉代码
        if ($exception instanceof ThrottleException) {
            return response([
                'code' => $exception->getCode(),
                'msg' => $exception->getMessage()
            ], 429);
        }

        return parent::render($request, $exception);
    }


}

  • 4 增加完以后在app.php中增加中间件:

在这里插入图片描述

// 认证中间件
$app->routeMiddleware([
    'auth' => App\Http\Middleware\Authenticate::class,
    'user_authenticate' => App\Http\Middleware\UserAuthenticateMiddleware::class,
    'xss' => App\Http\Middleware\XSSProtectionMiddleware::class,
    'language' => App\Http\Middleware\LanguageMiddleware::class,
    'currency' => App\Http\Middleware\CurrencyMiddleware::class,
    'throttle' => App\Http\Middleware\ThrottleRequestsMiddleware::class,//此处增加限流中间件
]);
  • 5 在路由相关位置增加使用的中间件即可。

如下:

$router->group([
    'middleware' => ['throttle:60,1'],
],function () use ($router){
	//联系我们
    $router->group(['prefix' => 'contact','as'=>'contact_us'],function () use ($router){
         $router->post('/us', 'ContactUsController@contact');
    });
}

以上代码是关于lumen限流API中间件的,都已经整理好了,复制粘贴即可使用。也可以根据自己需求进行调整。

文章来源:https://blog.csdn.net/t_fengyun/article/details/135763591
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。