Created
November 2, 2020 06:25
-
-
Save Oranzh/5c077cc73bd5f97bb1a0bbad26b0c954 to your computer and use it in GitHub Desktop.
Lumen Throttle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Http\Middleware; | |
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 Illuminate\Support\Str; | |
use RuntimeException; | |
use Symfony\Component\HttpFoundation\Response; | |
class ThrottleRequests | |
{ | |
use InteractsWithTime; | |
/** | |
* The rate limiter instance. | |
* | |
* @var \Illuminate\Cache\RateLimiter | |
*/ | |
protected $limiter; | |
/** | |
* Create a new request throttler. | |
* | |
* @param \Illuminate\Cache\RateLimiter $limiter | |
* @return void | |
*/ | |
public function __construct(RateLimiter $limiter) | |
{ | |
$this->limiter = $limiter; | |
} | |
/** | |
* 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), | |
'decayMinutes' => $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 = call_user_func($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' => md5($limiterName.$limit->key), | |
'maxAttempts' => $limit->maxAttempts, | |
'decayMinutes' => $limit->decayMinutes, | |
'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->decayMinutes * 60); | |
} | |
$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; | |
} | |
/** | |
return sha1( | |
$request->method() . | |
'|' . $request->server('SERVER_NAME') . | |
'|' . $request->path() . | |
'|' . $request->ip() | |
); | |
* **/ | |
/** | |
* Resolve request signature. | |
* | |
* @param \Illuminate\Http\Request $request | |
* @return string | |
* | |
* @throws \RuntimeException | |
*/ | |
protected function resolveRequestSignature($request) | |
{ | |
if ($user = $request->user()) { | |
var_dump('user'); | |
return sha1($user->getAuthIdentifier()); | |
} elseif($route = $request->route()) { | |
return sha1( | |
$request->method() . | |
'|' . $request->server('SERVER_NAME') . | |
'|' . $request->path() . | |
'|' . $request->ip() | |
); | |
} | |
throw new RuntimeException('Unable to generate the request signature. Route unavailable.'); | |
} | |
/** | |
* 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 | |
*/ | |
protected function buildException($request, $key, $maxAttempts, $responseCallback = null) | |
{ | |
$retryAfter = $this->getTimeUntilNextRetry($key); | |
$headers = $this->getHeaders( | |
$maxAttempts, | |
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter), | |
$retryAfter | |
); | |
return is_callable($responseCallback) | |
? new HttpResponseException($responseCallback($request, $headers)) | |
: new ThrottleRequestsException('Too Many Attempts.', null, $headers); | |
} | |
/** | |
* 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 $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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
第一步,composer require graham-campbell/throttle:^8.1
第二步,创建ThrottleRequests中间件,并且在bootstart文件中注册throttle
第三步,route引入throttle中间件,$router->get('posts', [ 'uses' => 'PostController@index', 'middleware' => 'throttle:5,2']), 表示2分钟内请求5次
根据自己用的cache,查询会有多出来的两个key,,然后获取这两个Key的值,一个是起始时间,另一个是已请求的次数