背景
现在公司重构api项目,针对有些写入和请求的接口需要进行限制设置。比如说一分钟60次等。看了网上的都是laravel的throttle限流,但是没有针对lumen的,所以需要自己重新封装。
实现
- 1.在AppHttpMiddleware下创建一个自定义的中间件,名为ThrottleRequestsMiddleware。
- 2 将如下自定义的中间件代码复制粘贴到中间件内。
备注:这里代码已经封装改写好了,复制粘贴既可以用
<?php namespace AppHttpMiddleware; use AppExceptionsThrottleException; use Closure; use IlluminateCacheRateLimiter; use IlluminateCacheRateLimitingUnlimited; use IlluminateHttpExceptionsHttpResponseException; use IlluminateHttpExceptionsThrottleRequestsException; use IlluminateSupportArr; use IlluminateSupportInteractsWithTime; use RuntimeException; use SymfonyComponentHttpFoundationResponse; class ThrottleRequestsMiddleware { use InteractsWithTime; /** * The rate limiter instance. * * @var IlluminateCacheRateLimiter */ protected $limiter; /** * Indicates if the rate limiter keys should be hashed. * * @var bool */ protected static $shouldHashKeys = true; /** * Create a new request throttler. * * @param IlluminateCacheRateLimiter $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 IlluminateHttpRequest $request * @param Closure $next * @param int|string $maxAttempts * @param float|int $decayMinutes * @param string $prefix * @return SymfonyComponentHttpFoundationResponse * * @throws IlluminateHttpExceptionsThrottleRequestsException */ 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 IlluminateHttpRequest $request * @param Closure $next * @param string $limiterName * @param Closure $limiter * @return SymfonyComponentHttpFoundationResponse * * @throws IlluminateHttpExceptionsThrottleRequestsException */ 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 IlluminateHttpRequest $request * @param Closure $next * @param array $limits * @return SymfonyComponentHttpFoundationResponse * * @throws IlluminateHttpExceptionsThrottleRequestsException */ 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 IlluminateHttpRequest $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 IlluminateHttpRequest $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 IlluminateHttpRequest $request * @param string $key * @param int $maxAttempts * @param callable|null $responseCallback * @return IlluminateHttpExceptionsThrottleRequestsException|IlluminateHttpExceptionsHttpResponseException */ 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 SymfonyComponentHttpFoundationResponse $response * @param int $maxAttempts * @param int $remainingAttempts * @param int|null $retryAfter * @return SymfonyComponentHttpFoundationResponse */ 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 SymfonyComponentHttpFoundationResponse|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超过限制时抛出的是IlluminateHttpExceptionsThrottleRequestsException,因为Lumen框架缺少这个文件,需要自己定义一下,在app/Exceptions中新建ThrottleException.php,写入以下代码:
具体位置:
具体代码:
<?php namespace AppExceptions; use Exception; class ThrottleException extends Exception { protected $isReport = false; public function isReport(){ return $this->isReport; } }
以上代码复制粘贴即可用。
- 3 同时需要在app/Exceptions/Handler.php捕获该抛出异常,在render方法增加以下判断:
代码如下:
<?php namespace AppExceptions; use IlluminateAuthAccessAuthorizationException; use IlluminateDatabaseEloquentModelNotFoundException; use IlluminateValidationValidationException; use LaravelLumenExceptionsHandler as ExceptionHandler; use SymfonyComponentHttpKernelExceptionHttpException; 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 IlluminateHttpRequest $request * @param Throwable $exception * @return IlluminateHttpResponse|IlluminateHttpJsonResponse * * @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' => AppHttpMiddlewareAuthenticate::class, 'user_authenticate' => AppHttpMiddlewareUserAuthenticateMiddleware::class, 'xss' => AppHttpMiddlewareXSSProtectionMiddleware::class, 'language' => AppHttpMiddlewareLanguageMiddleware::class, 'currency' => AppHttpMiddlewareCurrencyMiddleware::class, 'throttle' => AppHttpMiddlewareThrottleRequestsMiddleware::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中间件的,都已经整理好了,复制粘贴即可使用。也可以根据自己需求进行调整。