Laravel:从自定义中间件油门组返回 bool

Posted

技术标签:

【中文标题】Laravel:从自定义中间件油门组返回 bool【英文标题】:Laravel: return bool from custom middleware throttle group 【发布时间】:2022-01-09 00:14:29 【问题描述】:

我想在我的 Laravel 应用的中间件路由组中实现我自己的自定义逻辑。

我现在可以用 Laravel 开箱即用:

Route::middleware('throttle:10,1')->group(function () 
   Route::get('/test/throttle', function() 
      return response('OK', 200)->header('Content-Type', 'text/html');
   );
);

因此,正如预期的那样,它将默认的 429 代码视图返回为 HttpResponseException。


但我想覆盖油门中间件并只返回一个 (bool) tooManyAttempts 值作为函数参数:

Route::middleware('customthrottle:10,1')->group(function ($tooManyAttempts) 

   Route::get('/test/throttle', function() 
      if ($tooManyAttempts) 
         return response("My custom 'Too many attempts' page only for this route", 200)->header('Content-Type', 'text/html');
       else 
         return response('You are good to go yet, he-he', 200)->header('Content-Type', 'text/html');
      
   );
);

我阅读了this article,但我不知道如何以可以将一些值作为函数参数传递的方式覆盖它。它可能是 (bool) $tooManyAttempts value 或至少 (int) $maxAttempts value。


究竟是什么方法,我应该以什么方式重写来做到这一点? (vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php)

<?php

namespace Illuminate\Routing\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;
    

    /**
     * Resolve request signature.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string
     *
     * @throws \RuntimeException
     */
    protected function resolveRequestSignature($request)
    
        if ($user = $request->user()) 
            return sha1($user->getAuthIdentifier());
         elseif ($route = $request->route()) 
            return sha1($route->getDomain().'|'.$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|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;
    


【问题讨论】:

这是一个 xy 问题,您已经得出结论,您的中间件需要返回一个布尔值。您的问题不是更多的是您需要在限制请求时自定义响应吗? 感谢您的回复。我认为这是一个非常简单的问题,我只是没有经验来解决这些任务。我也找不到任何与此问题类似的可靠示例。此外,覆盖中间件组以返回 bool 值将是此任务最方便的方法。但我会很感激任何建议,即使是定制油门响应的例子,它不会破坏所需的逻辑。 但是响应是问题还是您想要实现的目标,而不谈论潜在的解决方案? 【参考方案1】:

按照 cmets 中的建议,您可以通过一种方法来实现您在控制器逻辑中所做的事情,即覆盖 Exceptions/Handler.php 类中的异常。

public function render($request, Exception $exception)

    if ($exception instanceof ThrottleRequestsException) 
         return response('Too many attempts', 200)->header('Content-Type', 'text/html');
    

【讨论】:

如果这会影响所有中间件组的所有限制异常,我是对的吗?如果是这样,不幸的是,这并不能解决问题。

以上是关于Laravel:从自定义中间件油门组返回 bool的主要内容,如果未能解决你的问题,请参考以下文章

.Net Core:从自定义异常中间件返回 IActionResult

Laravel 4.2 视图:从自定义位置发布

Laravel,如何从自定义表的 jwt 令牌获取登录用户

laravel 在使用 php artisan 时从自定义存根创建模型

从自定义转换中引用presentingViewController

在Laravel中使用Middleware进行身份验证