LoginThrottle.php 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
  1. <?php
  2. declare(strict_types=1);
  3. namespace app\miniapi\auth;
  4. use app\miniapi\exception\ApiException;
  5. /** Atomic, bounded, shared by workers on one host. Use a shared limiter before scaling to multiple hosts. */
  6. final class LoginThrottle
  7. {
  8. public function consume(string $ip): void
  9. {
  10. $path = (string) config('mini_api.login_throttle_file', runtime_path('mini-login-rate.json'));
  11. $file = @fopen($path, 'c+');
  12. if (!$file) {
  13. throw new ApiException(50302, '登录服务暂时不可用', 503);
  14. }
  15. try {
  16. if (!flock($file, LOCK_EX)) {
  17. throw new ApiException(50302, '登录服务暂时不可用', 503);
  18. }
  19. $window = intdiv(time(), 60);
  20. $state = json_decode(stream_get_contents($file), true);
  21. if (!is_array($state) || ($state['window'] ?? null) !== $window) {
  22. $state = [
  23. 'window' => $window,
  24. 'total' => 0,
  25. 'clients' => [],
  26. ];
  27. }
  28. $key = hash('sha256', $ip);
  29. if ($state['total'] >= (int) config('mini_api.login_global_per_minute', 600)
  30. || ($state['clients'][$key] ?? 0) >= (int) config('mini_api.login_ip_per_minute', 60)
  31. || (!isset($state['clients'][$key]) && count($state['clients']) >= 4096)) {
  32. throw new ApiException(42901, '登录请求过于频繁,请稍后再试', 429);
  33. }
  34. $state['total']++;
  35. $state['clients'][$key] = ($state['clients'][$key] ?? 0) + 1;
  36. $json = json_encode($state, JSON_THROW_ON_ERROR);
  37. rewind($file);
  38. if (!ftruncate($file, 0) || fwrite($file, $json) !== strlen($json) || !fflush($file)) {
  39. throw new ApiException(50302, '登录服务暂时不可用', 503);
  40. }
  41. } finally {
  42. flock($file, LOCK_UN);
  43. fclose($file);
  44. }
  45. }
  46. }