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, ]); } }