| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950 |
- <?php
- declare(strict_types=1);
- namespace app\miniapi\auth;
- use app\miniapi\exception\ApiException;
- /** Atomic, bounded, shared by workers on one host. Use a shared limiter before scaling to multiple hosts. */
- final class LoginThrottle
- {
- public function consume(string $ip): void
- {
- $path = (string) config('mini_api.login_throttle_file', runtime_path('mini-login-rate.json'));
- $file = @fopen($path, 'c+');
- if (!$file) {
- throw new ApiException(50302, '登录服务暂时不可用', 503);
- }
- try {
- if (!flock($file, LOCK_EX)) {
- throw new ApiException(50302, '登录服务暂时不可用', 503);
- }
- $window = intdiv(time(), 60);
- $state = json_decode(stream_get_contents($file), true);
- if (!is_array($state) || ($state['window'] ?? null) !== $window) {
- $state = [
- 'window' => $window,
- 'total' => 0,
- 'clients' => [],
- ];
- }
- $key = hash('sha256', $ip);
- if ($state['total'] >= (int) config('mini_api.login_global_per_minute', 600)
- || ($state['clients'][$key] ?? 0) >= (int) config('mini_api.login_ip_per_minute', 60)
- || (!isset($state['clients'][$key]) && count($state['clients']) >= 4096)) {
- throw new ApiException(42901, '登录请求过于频繁,请稍后再试', 429);
- }
- $state['total']++;
- $state['clients'][$key] = ($state['clients'][$key] ?? 0) + 1;
- $json = json_encode($state, JSON_THROW_ON_ERROR);
- rewind($file);
- if (!ftruncate($file, 0) || fwrite($file, $json) !== strlen($json) || !fflush($file)) {
- throw new ApiException(50302, '登录服务暂时不可用', 503);
- }
- } finally {
- flock($file, LOCK_UN);
- fclose($file);
- }
- }
- }
|