RateLimitService.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. <?php
  2. namespace App\Module\OpenAPI\Services;
  3. use App\Module\OpenAPI\Models\OpenApiApp;
  4. use App\Module\OpenAPI\Models\OpenApiRateLimit;
  5. use Illuminate\Support\Facades\Cache;
  6. use Illuminate\Support\Facades\Log;
  7. use Illuminate\Http\Request;
  8. /**
  9. * 频率限制服务
  10. *
  11. * 提供API调用频率限制功能,支持多种限制策略
  12. */
  13. class RateLimitService
  14. {
  15. /**
  16. * 检查频率限制
  17. *
  18. * @param OpenApiApp $app
  19. * @param Request $request
  20. * @param array|null $customLimits 自定义限制规则
  21. * @return array ['allowed' => bool, 'remaining' => int, 'reset_time' => int]
  22. */
  23. public function checkRateLimit(OpenApiApp $app, Request $request, ?array $customLimits = null): array
  24. {
  25. $ip = $request->ip();
  26. $appId = $app->app_id;
  27. $endpoint = $request->path();
  28. // 获取限制配置
  29. $limits = $customLimits ?? $this->getAppRateLimits($app);
  30. $result = [
  31. 'allowed' => true,
  32. 'remaining' => 0,
  33. 'reset_time' => 0,
  34. 'limit_type' => null,
  35. ];
  36. // 检查各种限制
  37. foreach ($limits as $limitType => $limitValue) {
  38. if ($limitValue <= 0) continue;
  39. $checkResult = $this->checkSpecificLimit($appId, $ip, $endpoint, $limitType, $limitValue);
  40. if (!$checkResult['allowed']) {
  41. $result = $checkResult;
  42. $result['limit_type'] = $limitType;
  43. break;
  44. }
  45. // 记录最小的剩余次数
  46. if ($result['remaining'] === 0 || $checkResult['remaining'] < $result['remaining']) {
  47. $result['remaining'] = $checkResult['remaining'];
  48. $result['reset_time'] = $checkResult['reset_time'];
  49. }
  50. }
  51. // 记录限制检查日志
  52. $this->logRateLimitCheck($app, $request, $result);
  53. return $result;
  54. }
  55. /**
  56. * 检查特定类型的限制
  57. *
  58. * @param string $appId
  59. * @param string $ip
  60. * @param string $endpoint
  61. * @param string $limitType
  62. * @param int $limitValue
  63. * @return array
  64. */
  65. protected function checkSpecificLimit(string $appId, string $ip, string $endpoint, string $limitType, int $limitValue): array
  66. {
  67. $window = $this->getLimitWindow($limitType);
  68. $key = $this->buildCacheKey($appId, $ip, $endpoint, $limitType);
  69. $current = Cache::get($key, 0);
  70. $remaining = max(0, $limitValue - $current);
  71. $resetTime = time() + $window;
  72. if ($current >= $limitValue) {
  73. return [
  74. 'allowed' => false,
  75. 'remaining' => 0,
  76. 'reset_time' => $resetTime,
  77. ];
  78. }
  79. // 增加计数
  80. Cache::put($key, $current + 1, $window);
  81. return [
  82. 'allowed' => true,
  83. 'remaining' => $remaining - 1,
  84. 'reset_time' => $resetTime,
  85. ];
  86. }
  87. /**
  88. * 获取应用的频率限制配置
  89. *
  90. * @param OpenApiApp $app
  91. * @return array
  92. */
  93. protected function getAppRateLimits(OpenApiApp $app): array
  94. {
  95. $rateLimits = $app->rate_limits ?? [];
  96. // 如果应用没有配置限制,使用默认配置
  97. if (empty($rateLimits)) {
  98. $rateLimits = config('openapi.rate_limit.default', [
  99. 'requests_per_minute' => 60,
  100. 'requests_per_hour' => 1000,
  101. 'requests_per_day' => 10000,
  102. ]);
  103. }
  104. return $rateLimits;
  105. }
  106. /**
  107. * 获取限制时间窗口(秒)
  108. *
  109. * @param string $limitType
  110. * @return int
  111. */
  112. protected function getLimitWindow(string $limitType): int
  113. {
  114. return match ($limitType) {
  115. 'requests_per_minute' => 60,
  116. 'requests_per_hour' => 3600,
  117. 'requests_per_day' => 86400,
  118. 'requests_per_week' => 604800,
  119. 'requests_per_month' => 2592000,
  120. default => 3600, // 默认1小时
  121. };
  122. }
  123. /**
  124. * 构建缓存键
  125. *
  126. * @param string $appId
  127. * @param string $ip
  128. * @param string $endpoint
  129. * @param string $limitType
  130. * @return string
  131. */
  132. protected function buildCacheKey(string $appId, string $ip, string $endpoint, string $limitType): string
  133. {
  134. $timeWindow = $this->getTimeWindow($limitType);
  135. return "rate_limit:{$limitType}:{$appId}:{$ip}:{$endpoint}:{$timeWindow}";
  136. }
  137. /**
  138. * 获取时间窗口标识
  139. *
  140. * @param string $limitType
  141. * @return string
  142. */
  143. protected function getTimeWindow(string $limitType): string
  144. {
  145. return match ($limitType) {
  146. 'requests_per_minute' => date('Y-m-d-H-i'),
  147. 'requests_per_hour' => date('Y-m-d-H'),
  148. 'requests_per_day' => date('Y-m-d'),
  149. 'requests_per_week' => date('Y-W'),
  150. 'requests_per_month' => date('Y-m'),
  151. default => date('Y-m-d-H'),
  152. };
  153. }
  154. /**
  155. * 记录频率限制检查日志
  156. *
  157. * @param OpenApiApp $app
  158. * @param Request $request
  159. * @param array $result
  160. * @return void
  161. */
  162. protected function logRateLimitCheck(OpenApiApp $app, Request $request, array $result): void
  163. {
  164. if (!$result['allowed']) {
  165. Log::warning('Rate limit exceeded', [
  166. 'app_id' => $app->app_id,
  167. 'ip' => $request->ip(),
  168. 'endpoint' => $request->path(),
  169. 'limit_type' => $result['limit_type'] ?? 'unknown',
  170. 'user_agent' => $request->userAgent(),
  171. ]);
  172. // 记录到数据库
  173. $this->recordRateLimitViolation($app, $request, $result);
  174. }
  175. }
  176. /**
  177. * 记录频率限制违规
  178. *
  179. * @param OpenApiApp $app
  180. * @param Request $request
  181. * @param array $result
  182. * @return void
  183. */
  184. protected function recordRateLimitViolation(OpenApiApp $app, Request $request, array $result): void
  185. {
  186. try {
  187. OpenApiRateLimit::create([
  188. 'app_id' => $app->app_id,
  189. 'ip_address' => $request->ip(),
  190. 'endpoint' => $request->path(),
  191. 'limit_type' => $result['limit_type'] ?? 'unknown',
  192. 'window_start' => now(),
  193. 'request_count' => 1,
  194. 'is_blocked' => true,
  195. 'user_agent' => $request->userAgent(),
  196. 'headers' => json_encode($request->headers->all()),
  197. ]);
  198. } catch (\Exception $e) {
  199. Log::error('Failed to record rate limit violation', [
  200. 'error' => $e->getMessage(),
  201. 'app_id' => $app->app_id,
  202. ]);
  203. }
  204. }
  205. /**
  206. * 获取应用的限流统计
  207. *
  208. * @param string $appId
  209. * @param string $period 统计周期:hour, day, week, month
  210. * @return array
  211. */
  212. public function getRateLimitStats(string $appId, string $period = 'day'): array
  213. {
  214. $startTime = $this->getStatsPeriodStart($period);
  215. $stats = OpenApiRateLimit::where('app_id', $appId)
  216. ->where('window_start', '>=', $startTime)
  217. ->selectRaw('
  218. limit_type,
  219. COUNT(*) as violation_count,
  220. SUM(request_count) as total_requests,
  221. COUNT(DISTINCT ip_address) as unique_ips
  222. ')
  223. ->groupBy('limit_type')
  224. ->get();
  225. return [
  226. 'period' => $period,
  227. 'start_time' => $startTime,
  228. 'stats' => $stats->toArray(),
  229. 'total_violations' => $stats->sum('violation_count'),
  230. 'total_blocked_requests' => $stats->sum('total_requests'),
  231. ];
  232. }
  233. /**
  234. * 获取统计周期的开始时间
  235. *
  236. * @param string $period
  237. * @return \Carbon\Carbon
  238. */
  239. protected function getStatsPeriodStart(string $period): \Carbon\Carbon
  240. {
  241. return match ($period) {
  242. 'hour' => now()->subHour(),
  243. 'day' => now()->subDay(),
  244. 'week' => now()->subWeek(),
  245. 'month' => now()->subMonth(),
  246. default => now()->subDay(),
  247. };
  248. }
  249. /**
  250. * 清理过期的限流记录
  251. *
  252. * @param int $daysToKeep 保留天数
  253. * @return int 清理的记录数
  254. */
  255. public function cleanExpiredRecords(int $daysToKeep = 30): int
  256. {
  257. $cutoffDate = now()->subDays($daysToKeep);
  258. return OpenApiRateLimit::where('window_start', '<', $cutoffDate)->delete();
  259. }
  260. /**
  261. * 重置应用的限流计数
  262. *
  263. * @param string $appId
  264. * @param string|null $limitType 限制类型,null表示重置所有
  265. * @return void
  266. */
  267. public function resetRateLimit(string $appId, ?string $limitType = null): void
  268. {
  269. $pattern = $limitType
  270. ? "rate_limit:{$limitType}:{$appId}:*"
  271. : "rate_limit:*:{$appId}:*";
  272. // 清除缓存
  273. $keys = Cache::getRedis()->keys($pattern);
  274. if (!empty($keys)) {
  275. Cache::getRedis()->del($keys);
  276. }
  277. Log::info('Rate limit reset', [
  278. 'app_id' => $appId,
  279. 'limit_type' => $limitType ?? 'all',
  280. 'cleared_keys' => count($keys),
  281. ]);
  282. }
  283. /**
  284. * 检查IP是否被临时封禁
  285. *
  286. * @param string $ip
  287. * @param string $appId
  288. * @return bool
  289. */
  290. public function isIpTemporarilyBanned(string $ip, string $appId): bool
  291. {
  292. $banKey = "ip_ban:{$appId}:{$ip}";
  293. return Cache::has($banKey);
  294. }
  295. /**
  296. * 临时封禁IP
  297. *
  298. * @param string $ip
  299. * @param string $appId
  300. * @param int $minutes 封禁分钟数
  301. * @param string $reason 封禁原因
  302. * @return void
  303. */
  304. public function temporarilyBanIp(string $ip, string $appId, int $minutes = 60, string $reason = 'Rate limit exceeded'): void
  305. {
  306. $banKey = "ip_ban:{$appId}:{$ip}";
  307. Cache::put($banKey, [
  308. 'reason' => $reason,
  309. 'banned_at' => now(),
  310. 'expires_at' => now()->addMinutes($minutes),
  311. ], $minutes * 60);
  312. Log::warning('IP temporarily banned', [
  313. 'ip' => $ip,
  314. 'app_id' => $appId,
  315. 'duration_minutes' => $minutes,
  316. 'reason' => $reason,
  317. ]);
  318. }
  319. /**
  320. * 解除IP封禁
  321. *
  322. * @param string $ip
  323. * @param string $appId
  324. * @return void
  325. */
  326. public function unbanIp(string $ip, string $appId): void
  327. {
  328. $banKey = "ip_ban:{$appId}:{$ip}";
  329. Cache::forget($banKey);
  330. Log::info('IP ban removed', [
  331. 'ip' => $ip,
  332. 'app_id' => $appId,
  333. ]);
  334. }
  335. }