diff --git a/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php b/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php index 6fbe19b37536..0538bdd764e0 100644 --- a/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php +++ b/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php @@ -30,6 +30,13 @@ class ThrottleRequestsWithRedis extends ThrottleRequests */ public $remaining = []; + /** + * The key prefix for Redis throttle keys. + * + * @var string + */ + protected $keyPrefix = ''; + /** * Create a new request throttler. * @@ -93,7 +100,7 @@ protected function handleRequest($request, Closure $next, array $limits) protected function tooManyAttempts($key, $maxAttempts, $decaySeconds) { $limiter = new DurationLimiter( - $this->getRedisConnection(), $key, $maxAttempts, $decaySeconds + $this->getRedisConnection(), $this->getKeyPrefix().$key, $maxAttempts, $decaySeconds ); return tap($limiter->tooManyAttempts(), function () use ($key, $limiter) { @@ -114,7 +121,7 @@ protected function tooManyAttempts($key, $maxAttempts, $decaySeconds) protected function hit($key, $maxAttempts, $decaySeconds) { $limiter = new DurationLimiter( - $this->getRedisConnection(), $key, $maxAttempts, $decaySeconds + $this->getRedisConnection(), $this->getKeyPrefix().$key, $maxAttempts, $decaySeconds ); $limiter->acquire(); @@ -148,6 +155,33 @@ protected function getTimeUntilNextRetry($key) return $this->decaysAt[$key] - $this->currentTime(); } + /** + * Get the prefix for Redis throttle keys. + * + * Override this method to provide a stable namespace for Redis keys, + * which is required in Redis environments where ACLs restrict access + * by key pattern. + * + * @return string + */ + protected function getKeyPrefix() + { + return $this->keyPrefix; + } + + /** + * Set the prefix for Redis throttle keys. + * + * @param string $prefix + * @return $this + */ + public function setKeyPrefix($prefix) + { + $this->keyPrefix = $prefix; + + return $this; + } + /** * Get the Redis connection that should be used for throttling. * diff --git a/tests/Integration/Http/ThrottleRequestsWithRedisTest.php b/tests/Integration/Http/ThrottleRequestsWithRedisTest.php index bf60a39f0739..98750f3416aa 100644 --- a/tests/Integration/Http/ThrottleRequestsWithRedisTest.php +++ b/tests/Integration/Http/ThrottleRequestsWithRedisTest.php @@ -92,4 +92,37 @@ public function testItReturnsConfiguredResponseWhenUsingAfterLimit(): void $this->get('/')->assertTooManyRequests()->assertContent('ah ah ah'); }); } + + public function testKeyPrefixIsPrependedToRedisKeys() + { + $this->ifRedisAvailable(function () { + Route::get('/', function () { + return 'yes'; + })->middleware(ThrottleRequestsWithRedis::class.':2,1'); + + // Set a key prefix on the middleware + $this->app->resolving(ThrottleRequestsWithRedis::class, function ($middleware) { + $middleware->setKeyPrefix('throttle:'); + }); + + $response = $this->withoutExceptionHandling()->get('/'); + $this->assertSame('yes', $response->getContent()); + + // Verify the middleware works correctly with the prefix + $response = $this->withoutExceptionHandling()->get('/'); + $this->assertSame('yes', $response->getContent()); + $this->assertEquals(0, $response->headers->get('X-RateLimit-Remaining')); + }); + } + + public function testSetKeyPrefixReturnsSelf() + { + $limiter = $this->app->make(\Illuminate\Cache\RateLimiter::class); + $redis = $this->app->make(\Illuminate\Contracts\Redis\Factory::class); + $middleware = new ThrottleRequestsWithRedis($limiter, $redis); + + $result = $middleware->setKeyPrefix('throttle:'); + + $this->assertSame($middleware, $result); + } }