Laravel 13 API 限流完全指南 API 限流是保护服务免受滥用的重要机制。本文将深入探讨 Laravel 13 中 API 限流的各种方法和最佳实践。
内置限流 路由限流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Route ::middleware ('throttle:60,1' )->group (function () { Route ::get ('/users' , [UserController ::class , 'index' ]); }); Route ::middleware ('throttle:60,1,user' )->group (function () { Route ::get ('/profile' , [ProfileController ::class , 'show' ]); }); Route ::middleware ('throttle:api' )->group (function () { Route ::apiResource ('posts' , PostController ::class ); });
限流配置 1 2 3 4 5 6 7 'ratelimiting' => [ 'api' => [ 'limit' => 60 , 'window' => 1 , ], ],
自定义限流器 定义限流器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 use Illuminate \Cache \RateLimiting \Limit ;use Illuminate \Support \Facades \RateLimiter ;public function boot ( ): void { RateLimiter ::for ('api' , function (Request $request ) { return Limit ::perMinute (60 ) ->by ($request ->user ()?->id ?: $request ->ip ()); }); RateLimiter ::for ('global' , function (Request $request ) { return Limit ::perMinute (1000 ); }); RateLimiter ::for ('login' , function (Request $request ) { return Limit ::perMinute (5 ) ->by ($request ->email . '|' . $request ->ip ()); }); RateLimiter ::for ('uploads' , function (Request $request ) { return $request ->user ()->isPremium () ? Limit ::perMinute (100 ) : Limit ::perMinute (10 ); }); }
使用自定义限流器 1 2 3 Route ::middleware (['throttle:login' ])->post ('/login' , [AuthController ::class , 'login' ]);Route ::middleware (['throttle:uploads' ])->post ('/upload' , [UploadController ::class , 'store' ]);
高级限流策略 动态限流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <?php namespace App \Services ;use Illuminate \Cache \RateLimiting \Limit ;use Illuminate \Support \Facades \RateLimiter ;use Illuminate \Http \Request ;class DynamicRateLimiter { public function register ( ): void { RateLimiter ::for ('dynamic' , function (Request $request ) { $user = $request ->user (); if (!$user ) { return Limit ::perMinute (30 )->by ($request ->ip ()); } return match ($user ->plan) { 'free' => Limit ::perMinute (60 ), 'basic' => Limit ::perMinute (200 ), 'premium' => Limit ::perMinute (1000 ), 'enterprise' => Limit ::none (), default => Limit ::perMinute (60 ), }; }); RateLimiter ::for ('burst' , function (Request $request ) { return [ Limit ::perSecond (10 )->by ($request ->ip ()), Limit ::perMinute (200 )->by ($request ->ip ()), ]; }); } }
滑动窗口限流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?php namespace App \Services ;use Illuminate \Support \Facades \Redis ;class SlidingWindowRateLimiter { protected string $prefix = 'rate_limit:' ; public function attempt (string $key , int $maxAttempts , int $decaySeconds ): bool { $now = microtime (true ); $windowStart = $now - $decaySeconds ; $redisKey = $this ->prefix . $key ; Redis ::multi (); Redis ::zRemRangeByScore ($redisKey , '-inf' , $windowStart ); Redis ::zAdd ($redisKey , $now , $this ->generateId ()); Redis ::expire ($redisKey , $decaySeconds ); $result = Redis ::zCard ($redisKey ); Redis ::exec (); return $result <= $maxAttempts ; } public function remaining (string $key , int $maxAttempts , int $decaySeconds ): int { $now = microtime (true ); $windowStart = $now - $decaySeconds ; $redisKey = $this ->prefix . $key ; Redis ::zRemRangeByScore ($redisKey , '-inf' , $windowStart ); $count = Redis ::zCard ($redisKey ); return max (0 , $maxAttempts - $count ); } protected function generateId ( ): string { return uniqid ('' , true ); } }
限流中间件 自定义限流中间件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <?php namespace App \Http \Middleware ;use Closure ;use Illuminate \Http \Request ;use Illuminate \Support \Facades \Cache ;use Symfony \Component \HttpFoundation \Response ;class CustomRateLimiter { public function handle (Request $request , Closure $next , int $maxAttempts = 60 , int $decayMinutes = 1 ): Response { $key = $this ->resolveKey ($request ); $attempts = Cache ::get ($key , 0 ); if ($attempts >= $maxAttempts ) { return $this ->buildResponse ($key , $maxAttempts , $decayMinutes ); } Cache ::put ($key , $attempts + 1 , $decayMinutes * 60 ); $response = $next ($request ); return $this ->addHeaders ($response , $maxAttempts , $attempts + 1 ); } protected function resolveKey (Request $request ): string { $identifier = $request ->user ()?->id ?? $request ->ip (); return 'rate_limit:' . sha1 ($identifier . '|' . $request ->path ()); } protected function buildResponse (string $key , int $maxAttempts , int $decayMinutes ): Response { $retryAfter = Cache ::get ("{$key} :timer" , now ()->addMinutes ($decayMinutes )->diffInSeconds (now ())); return response ()->json ([ 'message' => '请求过于频繁' , 'retry_after' => $retryAfter , ], 429 )->withHeaders ([ 'Retry-After' => $retryAfter , 'X-RateLimit-Limit' => $maxAttempts , 'X-RateLimit-Remaining' => 0 , ]); } protected function addHeaders (Response $response , int $maxAttempts , int $remaining ): Response { return $response ->withHeaders ([ 'X-RateLimit-Limit' => $maxAttempts , 'X-RateLimit-Remaining' => max (0 , $maxAttempts - $remaining ), ]); } }
分布式限流 Redis 限流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 <?php namespace App \Services ;use Illuminate \Support \Facades \Redis ;class RedisRateLimiter { protected string $prefix = 'rate_limit:' ; public function tooManyAttempts (string $key , int $maxAttempts ): bool { $redisKey = $this ->prefix . $key ; $attempts = Redis ::incr ($redisKey ); if ($attempts === 1 ) { Redis ::expire ($redisKey , 60 ); } return $attempts > $maxAttempts ; } public function hit (string $key , int $decaySeconds = 60 ): int { $redisKey = $this ->prefix . $key ; $attempts = Redis ::incr ($redisKey ); if ($attempts === 1 ) { Redis ::expire ($redisKey , $decaySeconds ); } return $attempts ; } public function attempts (string $key ): int { return (int ) Redis ::get ($this ->prefix . $key ) ?? 0 ; } public function resetAttempts (string $key ): void { Redis ::del ($this ->prefix . $key ); } public function availableIn (string $key ): int { return Redis ::ttl ($this ->prefix . $key ); } public function remaining (string $key , int $maxAttempts ): int { $attempts = $this ->attempts ($key ); return max (0 , $maxAttempts - $attempts ); } }
令牌桶限流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?php namespace App \Services ;use Illuminate \Support \Facades \Redis ;class TokenBucketRateLimiter { protected string $prefix = 'token_bucket:' ; public function __construct ( protected int $capacity = 100 , protected int $refillRate = 10 , protected int $refillTime = 1 ) {} public function attempt (string $key , int $tokens = 1 ): bool { $redisKey = $this ->prefix . $key ; $now = microtime (true ); $script = <<<'LUA' local key = KEYS[1] local capacity = tonumber(ARGV[1]) local rate = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local tokens = tonumber(ARGV[4]) local bucket = redis.call('HMGET', key, 'tokens', 'last_refill') local current_tokens = tonumber(bucket[1]) or capacity local last_refill = tonumber(bucket[2]) or now local elapsed = now - last_refill local refill = math.floor(elapsed * rate) current_tokens = math.min(capacity, current_tokens + refill) if current_tokens >= tokens then current_tokens = current_tokens - tokens redis.call('HMSET', key, 'tokens', current_tokens, 'last_refill', now) redis.call('EXPIRE', key, 3600) return {1, current_tokens} else return {0, current_tokens} end LUA ; $result = Redis ::eval ($script , 1 , $redisKey , $this ->capacity, $this ->refillRate, $now , $tokens ); return (bool ) $result [0 ]; } public function getTokens (string $key ): int { $tokens = Redis ::hget ($this ->prefix . $key , 'tokens' ); return (int ) $tokens ?? $this ->capacity; } }
限流监控 限流统计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <?php namespace App \Services ;use Illuminate \Support \Facades \Cache ;class RateLimitMonitor { protected string $statsKey = 'rate_limit_stats:' ; public function recordHit (string $key , bool $limited = false ): void { $statsKey = $this ->statsKey . $key ; $date = now ()->format ('Y-m-d' ); Cache ::increment ("{$statsKey} :{$date} :total" ); if ($limited ) { Cache ::increment ("{$statsKey} :{$date} :limited" ); } } public function getStats (string $key , ?string $date = null ): array { $date = $date ?? now ()->format ('Y-m-d' ); $statsKey = $this ->statsKey . $key ; return [ 'total' => Cache ::get ("{$statsKey} :{$date} :total" , 0 ), 'limited' => Cache ::get ("{$statsKey} :{$date} :limited" , 0 ), ]; } public function getTopLimited (int $limit = 10 ): array { return Cache ::get ('top_limited' , []); } }
限流响应 自定义限流响应 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 <?php namespace App \Http \Middleware ;use Closure ;use Illuminate \Http \Request ;use Illuminate \Cache \RateLimiter ;use Symfony \Component \HttpFoundation \Response ;class ThrottleRequests { public function __construct ( protected RateLimiter $limiter ) {} public function handle (Request $request , Closure $next , int $maxAttempts = 60 , int $decayMinutes = 1 , string $prefix = '' ): Response { $key = $prefix . $this ->resolveRequestSignature ($request ); if ($this ->limiter->tooManyAttempts ($key , $maxAttempts )) { return $this ->buildResponse ($key , $maxAttempts ); } $this ->limiter->hit ($key , $decayMinutes * 60 ); $response = $next ($request ); return $this ->addHeaders ( $response , $maxAttempts , $this ->calculateRemainingAttempts ($key , $maxAttempts ) ); } protected function buildResponse (string $key , int $maxAttempts ): Response { $retryAfter = $this ->limiter->availableIn ($key ); return response ()->json ([ 'success' => false , 'message' => '请求次数已达上限' , 'retry_after' => $retryAfter , 'limit' => $maxAttempts , ], 429 )->withHeaders ([ 'Retry-After' => $retryAfter , 'X-RateLimit-Limit' => $maxAttempts , 'X-RateLimit-Remaining' => 0 , 'X-RateLimit-Reset' => now ()->addSeconds ($retryAfter )->timestamp, ]); } protected function resolveRequestSignature (Request $request ): string { return sha1 ( ($request ->user ()?->id ?: $request ->ip ()) . '|' . $request ->path () ); } protected function calculateRemainingAttempts (string $key , int $maxAttempts ): int { return max (0 , $maxAttempts - $this ->limiter->attempts ($key ) + 1 ); } protected function addHeaders (Response $response , int $maxAttempts , int $remainingAttempts ): Response { return $response ->withHeaders ([ 'X-RateLimit-Limit' => $maxAttempts , 'X-RateLimit-Remaining' => $remainingAttempts , ]); } }
总结 Laravel 13 的 API 限流提供了:
内置限流中间件 自定义限流器定义 动态限流策略 滑动窗口算法 令牌桶算法 分布式 Redis 限流 限流监控统计 合理使用限流可以有效保护 API 免受滥用。