| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- <?php
- declare(strict_types=1);
- namespace app\miniapi\protection;
- use app\miniapi\exception\ApiException;
- use support\Log;
- /** Transport-independent quota policy. Identities must come from the verified session. */
- final class TrafficGuard
- {
- private static int $lastFailureLog = 0;
- public function check(string $scope, string $identity, string $endpoint, int $listCost, string $requestId, bool $listOnly = false): void
- {
- if (!config('mini_protection.enabled', false)) {
- return;
- }
- if (!in_array($scope, ['ip', 'account'], true)) {
- throw new \LogicException('Unknown protection scope');
- }
- $subject = hash('sha256', $identity);
- $buckets = $listOnly ? [] : [
- $this->bucket($scope . '_requests', $subject, 1),
- $this->bucket($scope . '_endpoint', $subject . ':' . hash('sha256', $endpoint), 1),
- ];
- if ($listCost > 0) {
- $buckets[] = $this->bucket($scope . '_list_rows', $subject, $listCost);
- }
- try {
- [$blocked, $retry, $notice] = (new RedisQuota())->consume($buckets);
- } catch (\Throwable $error) {
- // Do not log connection strings, request data or exception messages.
- if (time() - self::$lastFailureLog >= 60) {
- self::$lastFailureLog = time();
- Log::channel('mini_protection')->error('mini_api.protection_unavailable', [
- 'request_id' => $requestId,
- 'exception_type' => get_class($error),
- ]);
- }
- throw new ApiException(50303, '访问保护服务暂时不可用,请稍后重试', 503, ['retry_after' => 5]);
- }
- if (!$blocked) {
- return;
- }
- if ($notice) {
- Log::channel('mini_protection')->warning('mini_api.quota_exceeded', [
- 'request_id' => $requestId,
- 'scope' => $scope,
- 'subject_hash' => $subject,
- 'endpoint' => $endpoint,
- 'rule' => $buckets[$blocked - 1]['rule'],
- 'retry_after' => $retry,
- ]);
- }
- throw new ApiException(42902, '访问过于频繁,请在' . $retry . '秒后重试', 429, ['retry_after' => $retry]);
- }
- private function bucket(string $rule, string $subject, int $cost): array
- {
- [$limit, $seconds] = config('mini_protection.limits.' . $rule);
- return [
- 'key' => config('mini_protection.prefix') . $rule . ':' . $subject,
- 'limit' => (int) $limit,
- 'seconds' => (int) $seconds,
- 'cost' => $cost,
- 'rule' => $rule,
- ];
- }
- }
|