Laravel 速率限制为 return JSON 负载
Laravel rate limit to return a JSON payload
我正在 Laravel 之上构建一个 API。我想通过使用 Throttle
中间件来使用内置的速率限制功能。
问题是,当 throttle 中间件触发响应时:
// Response headers
Too Many Attempts.
我的 API 在 JSON 中使用了如下所示的错误负载:
// Response headers
{
"status": "error",
"error": {
"code": 404,
"message": "Resource not found."
}
}
让 Throttle
中间件以我需要的方式 return 输出的最佳方法是什么?
制作你自己的闪亮中间件,通过原始方式扩展它,并覆盖你喜欢被覆盖的方法。
$ php artisan make:middleware ThrottleRequests
打开 kernel.php 并删除(注释掉)原始中间件并添加你的。
ThrottleRequests.php
<?php
namespace App\Http\Middleware;
use Closure;
class ThrottleRequests extends \Illuminate\Routing\Middleware\ThrottleRequests
{
protected function buildResponse($key, $maxAttempts)
{
return parent::buildResponse($key, $maxAttempts); // TODO: Change the autogenerated stub
}
}
kernel.php
.
.
.
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
//'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
//'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'throttle' => \App\Http\Middleware\ThrottleRequests::class
];
所以我所做的是创建一个扩展 ThrottleRequest 中间件的自定义中间件。您可以覆盖 handle 函数来检查请求并查看它是否期望 JSON 作为响应。如果是这样,请调用 buildJsonResponse 函数,该函数将格式化 JSON 429 响应。您可以在 buildJsonResponse 中定制 JsonResponse 以满足您的 API 需求。
这允许您的 throttle 中间件处理 JSON 和其他响应。如果请求期望 JSON,它将 return 一个 JSON 响应,否则,它将 return 标准的“太多尝试”纯文本响应。
请注意,这来自 Laravel 5。6.x ThrottleRequests
中间件,基础 class 可能已从那时起发生变化。
编辑:
修复了在请求不期望 JSON 响应但请求受到限制的情况下对错误方法的调用。应该是 $this->buildException($key, $maxAttempts);
而不是 $this->buildResponse($key, $maxAttempts);
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Middleware\ThrottleRequests;
class ThrottlesRequest extends ThrottleRequests
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $maxAttempts
* @param float|int $decayMinutes
* @return mixed
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
// If the request expects JSON, build a JSON response, otherwise build standard response
if ($request->expectsJson()) {
return $this->buildJsonResponse($key, $maxAttempts);
} else {
return $this->buildException($key, $maxAttempts);
}
}
$this->limiter->hit($key, $decayMinutes);
$response = $next($request);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
/**
* Create a 'too many attempts' JSON response.
*
* @param string $key
* @param int $maxAttempts
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function buildJsonResponse($key, $maxAttempts)
{
$response = new JsonResponse([
'error' => [
'code' => 429,
'message' => 'Too Many Attempts.',
],
], 429);
$retryAfter = $this->limiter->availableIn($key);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
}
}
在app/Http/Middleware/中创建一个新文件ApiThrottleRequests.php并粘贴下面的代码:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
use Symfony\Component\HttpFoundation\Response;
class ApiThrottleRequests
{
/**
* The rate limiter instance.
*
* @var \Illuminate\Cache\RateLimiter
*/
protected $limiter;
/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
*/
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $maxAttempts
* @param int $decayMinutes
* @return mixed
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
return $this->buildResponse($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
*/
protected function resolveRequestSignature($request)
{
return $request->fingerprint();
}
/**
* Create a 'too many attempts' response.
*
* @param string $key
* @param int $maxAttempts
* @return \Illuminate\Http\Response
*/
protected function buildResponse($key, $maxAttempts)
{
$message = json_encode([
'error' => [
'message' => 'Too many attempts, please slow down the request.' //may comes from lang file
],
'status' => 4029 //your custom code
]);
$response = new Response($message, 429);
$retryAfter = $this->limiter->availableIn($key);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
}
/**
* 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 \Illuminate\Http\Response
*/
protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
{
$headers = [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
];
if (!is_null($retryAfter)) {
$headers['Retry-After'] = $retryAfter;
$headers['Content-Type'] = 'application/json';
}
$response->headers->add($headers);
return $response;
}
/**
* 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)
{
if (!is_null($retryAfter)) {
return 0;
}
return $this->limiter->retriesLeft($key, $maxAttempts);
}
}
然后转到 kernel.php 目录中的 app/Http/ 文件并替换
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
和
'throttle' => \App\Middleware\ApiThrottleRequests::class,
并使用它
middleware('throttle:60,1')
或添加
'apiThrottle' => \App\Http\Middleware\ApiThrottleRequests::class,
而你是这样使用的
middleware('apiThrottle:60,1')
并帮助link
我知道这是一个很老的问题,但我有一个非常简短的解决方案。
我们可以捕获并处理 Handler.php 和 return 相应 JSON 响应中的 ThrottleRequestsException,而不是创建新的中间件。
app\Exceptions\Hanlder.php
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use Request;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* @param \Throwable $exception
* @return void
*
* @throws \Throwable
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Throwable
*/
public function render($request, Throwable $exception)
{
if ($exception instanceof ThrottleRequestsException && $request->wantsJson()) {
return json_encode([
'message' => 'Too many attempts, please slow down the request.',
'status' => false
]);
}
return parent::render($request, $exception);
}
}
对于 laravel 8 上的任何人:
RateLimiter::for("verify",function(Request $request){
return Limit::perMinute(2)->by($request->ip())->response(function(){
return response()->json(["state"=>false,"error"=>"Youve tried too many times"],200);
});
});
然后将 throttle 中间件添加到你的路由中
我正在 Laravel 之上构建一个 API。我想通过使用 Throttle
中间件来使用内置的速率限制功能。
问题是,当 throttle 中间件触发响应时:
// Response headers
Too Many Attempts.
我的 API 在 JSON 中使用了如下所示的错误负载:
// Response headers
{
"status": "error",
"error": {
"code": 404,
"message": "Resource not found."
}
}
让 Throttle
中间件以我需要的方式 return 输出的最佳方法是什么?
制作你自己的闪亮中间件,通过原始方式扩展它,并覆盖你喜欢被覆盖的方法。
$ php artisan make:middleware ThrottleRequests
打开 kernel.php 并删除(注释掉)原始中间件并添加你的。
ThrottleRequests.php
<?php
namespace App\Http\Middleware;
use Closure;
class ThrottleRequests extends \Illuminate\Routing\Middleware\ThrottleRequests
{
protected function buildResponse($key, $maxAttempts)
{
return parent::buildResponse($key, $maxAttempts); // TODO: Change the autogenerated stub
}
}
kernel.php
.
.
.
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
//'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
//'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'throttle' => \App\Http\Middleware\ThrottleRequests::class
];
所以我所做的是创建一个扩展 ThrottleRequest 中间件的自定义中间件。您可以覆盖 handle 函数来检查请求并查看它是否期望 JSON 作为响应。如果是这样,请调用 buildJsonResponse 函数,该函数将格式化 JSON 429 响应。您可以在 buildJsonResponse 中定制 JsonResponse 以满足您的 API 需求。
这允许您的 throttle 中间件处理 JSON 和其他响应。如果请求期望 JSON,它将 return 一个 JSON 响应,否则,它将 return 标准的“太多尝试”纯文本响应。
请注意,这来自 Laravel 5。6.x ThrottleRequests
中间件,基础 class 可能已从那时起发生变化。
编辑:
修复了在请求不期望 JSON 响应但请求受到限制的情况下对错误方法的调用。应该是 $this->buildException($key, $maxAttempts);
而不是 $this->buildResponse($key, $maxAttempts);
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Middleware\ThrottleRequests;
class ThrottlesRequest extends ThrottleRequests
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $maxAttempts
* @param float|int $decayMinutes
* @return mixed
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
// If the request expects JSON, build a JSON response, otherwise build standard response
if ($request->expectsJson()) {
return $this->buildJsonResponse($key, $maxAttempts);
} else {
return $this->buildException($key, $maxAttempts);
}
}
$this->limiter->hit($key, $decayMinutes);
$response = $next($request);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
/**
* Create a 'too many attempts' JSON response.
*
* @param string $key
* @param int $maxAttempts
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function buildJsonResponse($key, $maxAttempts)
{
$response = new JsonResponse([
'error' => [
'code' => 429,
'message' => 'Too Many Attempts.',
],
], 429);
$retryAfter = $this->limiter->availableIn($key);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
}
}
在app/Http/Middleware/中创建一个新文件ApiThrottleRequests.php并粘贴下面的代码:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
use Symfony\Component\HttpFoundation\Response;
class ApiThrottleRequests
{
/**
* The rate limiter instance.
*
* @var \Illuminate\Cache\RateLimiter
*/
protected $limiter;
/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
*/
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $maxAttempts
* @param int $decayMinutes
* @return mixed
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
return $this->buildResponse($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
*/
protected function resolveRequestSignature($request)
{
return $request->fingerprint();
}
/**
* Create a 'too many attempts' response.
*
* @param string $key
* @param int $maxAttempts
* @return \Illuminate\Http\Response
*/
protected function buildResponse($key, $maxAttempts)
{
$message = json_encode([
'error' => [
'message' => 'Too many attempts, please slow down the request.' //may comes from lang file
],
'status' => 4029 //your custom code
]);
$response = new Response($message, 429);
$retryAfter = $this->limiter->availableIn($key);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
}
/**
* 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 \Illuminate\Http\Response
*/
protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
{
$headers = [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
];
if (!is_null($retryAfter)) {
$headers['Retry-After'] = $retryAfter;
$headers['Content-Type'] = 'application/json';
}
$response->headers->add($headers);
return $response;
}
/**
* 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)
{
if (!is_null($retryAfter)) {
return 0;
}
return $this->limiter->retriesLeft($key, $maxAttempts);
}
}
然后转到 kernel.php 目录中的 app/Http/ 文件并替换
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
和
'throttle' => \App\Middleware\ApiThrottleRequests::class,
并使用它
middleware('throttle:60,1')
或添加
'apiThrottle' => \App\Http\Middleware\ApiThrottleRequests::class,
而你是这样使用的
middleware('apiThrottle:60,1')
并帮助link
我知道这是一个很老的问题,但我有一个非常简短的解决方案。
我们可以捕获并处理 Handler.php 和 return 相应 JSON 响应中的 ThrottleRequestsException,而不是创建新的中间件。
app\Exceptions\Hanlder.php
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use Request;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* @param \Throwable $exception
* @return void
*
* @throws \Throwable
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Throwable
*/
public function render($request, Throwable $exception)
{
if ($exception instanceof ThrottleRequestsException && $request->wantsJson()) {
return json_encode([
'message' => 'Too many attempts, please slow down the request.',
'status' => false
]);
}
return parent::render($request, $exception);
}
}
对于 laravel 8 上的任何人:
RateLimiter::for("verify",function(Request $request){
return Limit::perMinute(2)->by($request->ip())->response(function(){
return response()->json(["state"=>false,"error"=>"Youve tried too many times"],200);
});
});
然后将 throttle 中间件添加到你的路由中