| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- <?php
- namespace App\Module\OpenAPI\Services;
- use App\Module\OpenAPI\Models\OpenApiApp;
- use App\Module\OpenAPI\Models\OpenApiRateLimit;
- use Illuminate\Support\Facades\Cache;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Http\Request;
- /**
- * 频率限制服务
- *
- * 提供API调用频率限制功能,支持多种限制策略
- */
- class RateLimitService
- {
- /**
- * 检查频率限制
- *
- * @param OpenApiApp $app
- * @param Request $request
- * @param array|null $customLimits 自定义限制规则
- * @return array ['allowed' => bool, 'remaining' => int, 'reset_time' => int]
- */
- public function checkRateLimit(OpenApiApp $app, Request $request, ?array $customLimits = null): array
- {
- $ip = $request->ip();
- $appId = $app->app_id;
- $endpoint = $request->path();
- // 获取限制配置
- $limits = $customLimits ?? $this->getAppRateLimits($app);
- $result = [
- 'allowed' => true,
- 'remaining' => 0,
- 'reset_time' => 0,
- 'limit_type' => null,
- ];
- // 检查各种限制
- foreach ($limits as $limitType => $limitValue) {
- if ($limitValue <= 0) continue;
- $checkResult = $this->checkSpecificLimit($appId, $ip, $endpoint, $limitType, $limitValue);
-
- if (!$checkResult['allowed']) {
- $result = $checkResult;
- $result['limit_type'] = $limitType;
- break;
- }
- // 记录最小的剩余次数
- if ($result['remaining'] === 0 || $checkResult['remaining'] < $result['remaining']) {
- $result['remaining'] = $checkResult['remaining'];
- $result['reset_time'] = $checkResult['reset_time'];
- }
- }
- // 记录限制检查日志
- $this->logRateLimitCheck($app, $request, $result);
- return $result;
- }
- /**
- * 检查特定类型的限制
- *
- * @param string $appId
- * @param string $ip
- * @param string $endpoint
- * @param string $limitType
- * @param int $limitValue
- * @return array
- */
- protected function checkSpecificLimit(string $appId, string $ip, string $endpoint, string $limitType, int $limitValue): array
- {
- $window = $this->getLimitWindow($limitType);
- $key = $this->buildCacheKey($appId, $ip, $endpoint, $limitType);
- $current = Cache::get($key, 0);
- $remaining = max(0, $limitValue - $current);
- $resetTime = time() + $window;
- if ($current >= $limitValue) {
- return [
- 'allowed' => false,
- 'remaining' => 0,
- 'reset_time' => $resetTime,
- ];
- }
- // 增加计数
- Cache::put($key, $current + 1, $window);
- return [
- 'allowed' => true,
- 'remaining' => $remaining - 1,
- 'reset_time' => $resetTime,
- ];
- }
- /**
- * 获取应用的频率限制配置
- *
- * @param OpenApiApp $app
- * @return array
- */
- protected function getAppRateLimits(OpenApiApp $app): array
- {
- $rateLimits = $app->rate_limits ?? [];
- // 如果应用没有配置限制,使用默认配置
- if (empty($rateLimits)) {
- $rateLimits = config('openapi.rate_limit.default', [
- 'requests_per_minute' => 60,
- 'requests_per_hour' => 1000,
- 'requests_per_day' => 10000,
- ]);
- }
- return $rateLimits;
- }
- /**
- * 获取限制时间窗口(秒)
- *
- * @param string $limitType
- * @return int
- */
- protected function getLimitWindow(string $limitType): int
- {
- return match ($limitType) {
- 'requests_per_minute' => 60,
- 'requests_per_hour' => 3600,
- 'requests_per_day' => 86400,
- 'requests_per_week' => 604800,
- 'requests_per_month' => 2592000,
- default => 3600, // 默认1小时
- };
- }
- /**
- * 构建缓存键
- *
- * @param string $appId
- * @param string $ip
- * @param string $endpoint
- * @param string $limitType
- * @return string
- */
- protected function buildCacheKey(string $appId, string $ip, string $endpoint, string $limitType): string
- {
- $timeWindow = $this->getTimeWindow($limitType);
- return "rate_limit:{$limitType}:{$appId}:{$ip}:{$endpoint}:{$timeWindow}";
- }
- /**
- * 获取时间窗口标识
- *
- * @param string $limitType
- * @return string
- */
- protected function getTimeWindow(string $limitType): string
- {
- return match ($limitType) {
- 'requests_per_minute' => date('Y-m-d-H-i'),
- 'requests_per_hour' => date('Y-m-d-H'),
- 'requests_per_day' => date('Y-m-d'),
- 'requests_per_week' => date('Y-W'),
- 'requests_per_month' => date('Y-m'),
- default => date('Y-m-d-H'),
- };
- }
- /**
- * 记录频率限制检查日志
- *
- * @param OpenApiApp $app
- * @param Request $request
- * @param array $result
- * @return void
- */
- protected function logRateLimitCheck(OpenApiApp $app, Request $request, array $result): void
- {
- if (!$result['allowed']) {
- Log::warning('Rate limit exceeded', [
- 'app_id' => $app->app_id,
- 'ip' => $request->ip(),
- 'endpoint' => $request->path(),
- 'limit_type' => $result['limit_type'] ?? 'unknown',
- 'user_agent' => $request->userAgent(),
- ]);
- // 记录到数据库
- $this->recordRateLimitViolation($app, $request, $result);
- }
- }
- /**
- * 记录频率限制违规
- *
- * @param OpenApiApp $app
- * @param Request $request
- * @param array $result
- * @return void
- */
- protected function recordRateLimitViolation(OpenApiApp $app, Request $request, array $result): void
- {
- try {
- OpenApiRateLimit::create([
- 'app_id' => $app->app_id,
- 'ip_address' => $request->ip(),
- 'endpoint' => $request->path(),
- 'limit_type' => $result['limit_type'] ?? 'unknown',
- 'window_start' => now(),
- 'request_count' => 1,
- 'is_blocked' => true,
- 'user_agent' => $request->userAgent(),
- 'headers' => json_encode($request->headers->all()),
- ]);
- } catch (\Exception $e) {
- Log::error('Failed to record rate limit violation', [
- 'error' => $e->getMessage(),
- 'app_id' => $app->app_id,
- ]);
- }
- }
- /**
- * 获取应用的限流统计
- *
- * @param string $appId
- * @param string $period 统计周期:hour, day, week, month
- * @return array
- */
- public function getRateLimitStats(string $appId, string $period = 'day'): array
- {
- $startTime = $this->getStatsPeriodStart($period);
- $stats = OpenApiRateLimit::where('app_id', $appId)
- ->where('window_start', '>=', $startTime)
- ->selectRaw('
- limit_type,
- COUNT(*) as violation_count,
- SUM(request_count) as total_requests,
- COUNT(DISTINCT ip_address) as unique_ips
- ')
- ->groupBy('limit_type')
- ->get();
- return [
- 'period' => $period,
- 'start_time' => $startTime,
- 'stats' => $stats->toArray(),
- 'total_violations' => $stats->sum('violation_count'),
- 'total_blocked_requests' => $stats->sum('total_requests'),
- ];
- }
- /**
- * 获取统计周期的开始时间
- *
- * @param string $period
- * @return \Carbon\Carbon
- */
- protected function getStatsPeriodStart(string $period): \Carbon\Carbon
- {
- return match ($period) {
- 'hour' => now()->subHour(),
- 'day' => now()->subDay(),
- 'week' => now()->subWeek(),
- 'month' => now()->subMonth(),
- default => now()->subDay(),
- };
- }
- /**
- * 清理过期的限流记录
- *
- * @param int $daysToKeep 保留天数
- * @return int 清理的记录数
- */
- public function cleanExpiredRecords(int $daysToKeep = 30): int
- {
- $cutoffDate = now()->subDays($daysToKeep);
- return OpenApiRateLimit::where('window_start', '<', $cutoffDate)->delete();
- }
- /**
- * 重置应用的限流计数
- *
- * @param string $appId
- * @param string|null $limitType 限制类型,null表示重置所有
- * @return void
- */
- public function resetRateLimit(string $appId, ?string $limitType = null): void
- {
- $pattern = $limitType
- ? "rate_limit:{$limitType}:{$appId}:*"
- : "rate_limit:*:{$appId}:*";
- // 清除缓存
- $keys = Cache::getRedis()->keys($pattern);
- if (!empty($keys)) {
- Cache::getRedis()->del($keys);
- }
- Log::info('Rate limit reset', [
- 'app_id' => $appId,
- 'limit_type' => $limitType ?? 'all',
- 'cleared_keys' => count($keys),
- ]);
- }
- /**
- * 检查IP是否被临时封禁
- *
- * @param string $ip
- * @param string $appId
- * @return bool
- */
- public function isIpTemporarilyBanned(string $ip, string $appId): bool
- {
- $banKey = "ip_ban:{$appId}:{$ip}";
- return Cache::has($banKey);
- }
- /**
- * 临时封禁IP
- *
- * @param string $ip
- * @param string $appId
- * @param int $minutes 封禁分钟数
- * @param string $reason 封禁原因
- * @return void
- */
- public function temporarilyBanIp(string $ip, string $appId, int $minutes = 60, string $reason = 'Rate limit exceeded'): void
- {
- $banKey = "ip_ban:{$appId}:{$ip}";
- Cache::put($banKey, [
- 'reason' => $reason,
- 'banned_at' => now(),
- 'expires_at' => now()->addMinutes($minutes),
- ], $minutes * 60);
- Log::warning('IP temporarily banned', [
- 'ip' => $ip,
- 'app_id' => $appId,
- 'duration_minutes' => $minutes,
- 'reason' => $reason,
- ]);
- }
- /**
- * 解除IP封禁
- *
- * @param string $ip
- * @param string $appId
- * @return void
- */
- public function unbanIp(string $ip, string $appId): void
- {
- $banKey = "ip_ban:{$appId}:{$ip}";
- Cache::forget($banKey);
- Log::info('IP ban removed', [
- 'ip' => $ip,
- 'app_id' => $appId,
- ]);
- }
- }
|