From 2d745971b2cf2d04e5b34f390a790426a73ccdc0 Mon Sep 17 00:00:00 2001 From: M1Screw <14369594+M1Screw@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:57:00 +0800 Subject: [PATCH] refactor: rate limit service --- app/predefine.php | 5 +- composer.lock | 88 +++++------ config/.config.example.php | 18 ++- src/Controllers/AuthController.php | 4 +- src/Controllers/PasswordController.php | 4 +- src/Controllers/SubController.php | 4 +- src/Controllers/User/TicketController.php | 2 +- src/Middleware/NodeToken.php | 4 +- src/Services/RateLimit.php | 183 ++++++++++++---------- 9 files changed, 167 insertions(+), 145 deletions(-) diff --git a/app/predefine.php b/app/predefine.php index 98fa7008e6..09e916e4bc 100644 --- a/app/predefine.php +++ b/app/predefine.php @@ -2,9 +2,6 @@ declare(strict_types=1); -/** - * To define global variable - */ - +// Global constants const BASE_PATH = __DIR__ . '/..'; const VERSION = '2024.1'; diff --git a/composer.lock b/composer.lock index 5ee0b8121a..7a73868a0e 100644 --- a/composer.lock +++ b/composer.lock @@ -622,16 +622,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.304.0", + "version": "3.304.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "29a79bac02997f3053559f6961a0e83622a14f88" + "reference": "6dac9b3257873a807ac73f6dc4418bdc49a5d9db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/29a79bac02997f3053559f6961a0e83622a14f88", - "reference": "29a79bac02997f3053559f6961a0e83622a14f88", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6dac9b3257873a807ac73f6dc4418bdc49a5d9db", + "reference": "6dac9b3257873a807ac73f6dc4418bdc49a5d9db", "shasum": "" }, "require": { @@ -711,9 +711,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.304.0" + "source": "https://github.com/aws/aws-sdk-php/tree/3.304.1" }, - "time": "2024-04-08T18:03:38+00:00" + "time": "2024-04-09T19:25:27+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1703,16 +1703,16 @@ }, { "name": "illuminate/collections", - "version": "v11.2.0", + "version": "v11.3.0", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "4cbbdae4ef3654378e28e7ebd43cad91adf4a658" + "reference": "aee944e8220588756e21aa4c30eebd5f6481e453" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/4cbbdae4ef3654378e28e7ebd43cad91adf4a658", - "reference": "4cbbdae4ef3654378e28e7ebd43cad91adf4a658", + "url": "https://api.github.com/repos/illuminate/collections/zipball/aee944e8220588756e21aa4c30eebd5f6481e453", + "reference": "aee944e8220588756e21aa4c30eebd5f6481e453", "shasum": "" }, "require": { @@ -1754,20 +1754,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-03-21T09:12:36+00:00" + "time": "2024-04-04T17:36:49+00:00" }, { "name": "illuminate/conditionable", - "version": "v11.2.0", + "version": "v11.3.0", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", - "reference": "e4c5c9b855c60c7bb16ce92ca9372684448cce47" + "reference": "8a558fec063b6a63da1c3af1d219c0f998edffeb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/conditionable/zipball/e4c5c9b855c60c7bb16ce92ca9372684448cce47", - "reference": "e4c5c9b855c60c7bb16ce92ca9372684448cce47", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/8a558fec063b6a63da1c3af1d219c0f998edffeb", + "reference": "8a558fec063b6a63da1c3af1d219c0f998edffeb", "shasum": "" }, "require": { @@ -1800,20 +1800,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2023-02-07T10:39:14+00:00" + "time": "2024-04-04T17:36:49+00:00" }, { "name": "illuminate/container", - "version": "v11.2.0", + "version": "v11.3.0", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", - "reference": "78cbe88cdc7300efd4cf90244abec2e3c42219bb" + "reference": "af979ecfd6dfa6583eae5dfe2e9a8840358f4ca7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/78cbe88cdc7300efd4cf90244abec2e3c42219bb", - "reference": "78cbe88cdc7300efd4cf90244abec2e3c42219bb", + "url": "https://api.github.com/repos/illuminate/container/zipball/af979ecfd6dfa6583eae5dfe2e9a8840358f4ca7", + "reference": "af979ecfd6dfa6583eae5dfe2e9a8840358f4ca7", "shasum": "" }, "require": { @@ -1851,20 +1851,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2023-09-15T13:17:41+00:00" + "time": "2024-04-04T17:36:49+00:00" }, { "name": "illuminate/contracts", - "version": "v11.2.0", + "version": "v11.3.0", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "fae548ad43f569fc506f40385b2e0dcf1f4eb2c9" + "reference": "28bc6fb6fe3debb27a19b12a59288ed2d1bd4008" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/fae548ad43f569fc506f40385b2e0dcf1f4eb2c9", - "reference": "fae548ad43f569fc506f40385b2e0dcf1f4eb2c9", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/28bc6fb6fe3debb27a19b12a59288ed2d1bd4008", + "reference": "28bc6fb6fe3debb27a19b12a59288ed2d1bd4008", "shasum": "" }, "require": { @@ -1899,20 +1899,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-02-23T15:52:10+00:00" + "time": "2024-04-04T17:36:49+00:00" }, { "name": "illuminate/database", - "version": "v11.2.0", + "version": "v11.3.0", "source": { "type": "git", "url": "https://github.com/illuminate/database.git", - "reference": "c7ee848e6a0bc5466c17549bdd118ca99283a65f" + "reference": "d9eabd0bb1d05b05f6d045b0e1d1d8e008e40eb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/database/zipball/c7ee848e6a0bc5466c17549bdd118ca99283a65f", - "reference": "c7ee848e6a0bc5466c17549bdd118ca99283a65f", + "url": "https://api.github.com/repos/illuminate/database/zipball/d9eabd0bb1d05b05f6d045b0e1d1d8e008e40eb1", + "reference": "d9eabd0bb1d05b05f6d045b0e1d1d8e008e40eb1", "shasum": "" }, "require": { @@ -1967,11 +1967,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-04-01T22:47:30+00:00" + "time": "2024-04-09T14:34:24+00:00" }, { "name": "illuminate/macroable", - "version": "v11.2.0", + "version": "v11.3.0", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -2017,7 +2017,7 @@ }, { "name": "illuminate/pagination", - "version": "v11.2.0", + "version": "v11.3.0", "source": { "type": "git", "url": "https://github.com/illuminate/pagination.git", @@ -2067,16 +2067,16 @@ }, { "name": "illuminate/support", - "version": "v11.2.0", + "version": "v11.3.0", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "abedd71609c48606735754192c15e7eb7c9f1248" + "reference": "0794cd2077e57cabe9dead2b081e7e20f71e4ffe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/abedd71609c48606735754192c15e7eb7c9f1248", - "reference": "abedd71609c48606735754192c15e7eb7c9f1248", + "url": "https://api.github.com/repos/illuminate/support/zipball/0794cd2077e57cabe9dead2b081e7e20f71e4ffe", + "reference": "0794cd2077e57cabe9dead2b081e7e20f71e4ffe", "shasum": "" }, "require": { @@ -2137,7 +2137,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-04-01T15:22:25+00:00" + "time": "2024-04-09T14:13:50+00:00" }, { "name": "irazasyed/telegram-bot-sdk", @@ -5038,16 +5038,16 @@ }, { "name": "stripe/stripe-php", - "version": "v13.17.0", + "version": "v13.18.0", "source": { "type": "git", "url": "https://github.com/stripe/stripe-php.git", - "reference": "24d3d9ed8406e10d24452af44597300173655f69" + "reference": "02abb043b103766f4ed920642ae56ffdc58c7467" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stripe/stripe-php/zipball/24d3d9ed8406e10d24452af44597300173655f69", - "reference": "24d3d9ed8406e10d24452af44597300173655f69", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/02abb043b103766f4ed920642ae56ffdc58c7467", + "reference": "02abb043b103766f4ed920642ae56ffdc58c7467", "shasum": "" }, "require": { @@ -5091,9 +5091,9 @@ ], "support": { "issues": "https://github.com/stripe/stripe-php/issues", - "source": "https://github.com/stripe/stripe-php/tree/v13.17.0" + "source": "https://github.com/stripe/stripe-php/tree/v13.18.0" }, - "time": "2024-04-04T22:16:10+00:00" + "time": "2024-04-09T21:08:04+00:00" }, { "name": "symfony/clock", diff --git a/config/.config.example.php b/config/.config.example.php index 89fa88d1fa..b769624673 100644 --- a/config/.config.example.php +++ b/config/.config.example.php @@ -45,13 +45,17 @@ $_ENV['redis_ssl_context'] = []; // 使用SSL时的上下文选项,参考 https://www.php.net/manual/zh/context.ssl.php //Rate Limit 设置-------------------------------------------------------------------------------------------------------- -$_ENV['enable_rate_limit'] = true; // 是否开启请求限制 -$_ENV['rate_limit_ip'] = 120; // 每分钟每个IP的全局请求限制 -$_ENV['rate_limit_sub'] = 30; // 每分钟每个用户的订阅链接请求限制 -$_ENV['rate_limit_webapi'] = 1200; // 每分钟WebAPI全局请求限制 -$_ENV['rate_limit_user_api'] = 60; // 每分钟每个用户的API请求限制 -$_ENV['rate_limit_admin_api'] = 60; // 每分钟每个管理员的API请求限制 -$_ENV['rate_limit_node_api'] = 120; // 每分钟每个节点的API请求限制 +$_ENV['enable_rate_limit'] = true; // 是否开启请求限制 +$_ENV['rate_limit_sub_ip'] = 10; // 每分钟每个IP的订阅链接请求限制 +$_ENV['rate_limit_sub'] = 10; // 每分钟每个用户的订阅链接请求限制 +$_ENV['rate_limit_webapi_ip'] = 120; // 每分钟每个IP的WebAPI请求限制 +$_ENV['rate_limit_webapi'] = 1200; // 每分钟WebAPI全局请求限制 +$_ENV['rate_limit_user_api_ip'] = 60; // 每分钟每个IP的用户API请求限制 +$_ENV['rate_limit_user_api'] = 60; // 每分钟每个用户的API请求限制 +$_ENV['rate_limit_admin_api_ip'] = 60; // 每分钟每个管理员的API请求限制 +$_ENV['rate_limit_admin_api'] = 60; // 每分钟每个管理员的API请求限制 +$_ENV['rate_limit_node_api_ip'] = 60; // 每分钟每个IP的节点API请求限制 +$_ENV['rate_limit_node_api'] = 60; // 每分钟每个节点的API请求限制 //邮件设置---------------------------------------------------------------------------------------------------------------- $_ENV['mail_filter'] = 0; // 0: 关闭; 1: 白名单模式; 2; 黑名单模式; diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index 54d345c67c..2c859ff8d0 100644 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -157,8 +157,8 @@ public function sendVerify(ServerRequest $request, Response $response, $next): R return ResponseHelper::error($response, '无效的邮箱'); } - if (! RateLimit::checkEmailIpLimit($request->getServerParam('REMOTE_ADDR')) || - ! RateLimit::checkEmailAddressLimit($email) + if (! (new RateLimit())->checkRateLimit('email_request_ip', $request->getServerParam('REMOTE_ADDR')) || + ! (new RateLimit())->checkRateLimit('email_request_address', $email) ) { return ResponseHelper::error($response, '你的请求过于频繁,请稍后再试'); } diff --git a/src/Controllers/PasswordController.php b/src/Controllers/PasswordController.php index fae71e3eca..e9611d1321 100644 --- a/src/Controllers/PasswordController.php +++ b/src/Controllers/PasswordController.php @@ -56,8 +56,8 @@ public function handleReset(ServerRequest $request, Response $response, array $a return ResponseHelper::error($response, '未填写邮箱'); } - if (! RateLimit::checkEmailIpLimit($request->getServerParam('REMOTE_ADDR')) || - ! RateLimit::checkEmailAddressLimit($email) + if (! (new RateLimit())->checkRateLimit('email_request_ip', $request->getServerParam('REMOTE_ADDR')) || + ! (new RateLimit())->checkRateLimit('email_request_address', $email) ) { return ResponseHelper::error($response, '你的请求过于频繁,请稍后再试'); } diff --git a/src/Controllers/SubController.php b/src/Controllers/SubController.php index 56c664e0da..7640649f5d 100644 --- a/src/Controllers/SubController.php +++ b/src/Controllers/SubController.php @@ -42,8 +42,8 @@ public function index($request, $response, $args): ResponseInterface $token = $this->antiXss->xss_clean($args['token']); if ($_ENV['enable_rate_limit'] && - (! RateLimit::checkIPLimit($request->getServerParam('REMOTE_ADDR')) || - ! RateLimit::checkSubLimit($token)) + (! (new RateLimit())->checkRateLimit('sub_ip', $request->getServerParam('REMOTE_ADDR')) || + ! (new RateLimit())->checkRateLimit('sub_token', $token)) ) { return ResponseHelper::error($response, $err_msg); } diff --git a/src/Controllers/User/TicketController.php b/src/Controllers/User/TicketController.php index 9d3f5a4977..948b0aa933 100644 --- a/src/Controllers/User/TicketController.php +++ b/src/Controllers/User/TicketController.php @@ -64,7 +64,7 @@ public function add(ServerRequest $request, Response $response, array $args): Re if (! Config::obtain('enable_ticket') || $this->user->is_shadow_banned || - ! RateLimit::checkTicketLimit($this->user->id) || + ! (new RateLimit())->checkRateLimit('ticket', (string) $this->user->id) || $title === '' || $comment === '' || $type === '' diff --git a/src/Middleware/NodeToken.php b/src/Middleware/NodeToken.php index ee2215564b..e225eda642 100644 --- a/src/Middleware/NodeToken.php +++ b/src/Middleware/NodeToken.php @@ -33,8 +33,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $antiXss = new AntiXSS(); if ($_ENV['enable_rate_limit'] && - (! RateLimit::checkIPLimit($request->getServerParam('REMOTE_ADDR')) || - ! RateLimit::checkWebAPILimit($antiXss->xss_clean($key))) + (! (new RateLimit())->checkRateLimit('webapi_ip', $request->getServerParam('REMOTE_ADDR')) || + ! (new RateLimit())->checkRateLimit('webapi_key', $antiXss->xss_clean($key))) ) { return AppFactory::determineResponseFactory()->createResponse(401)->withJson([ 'ret' => 0, diff --git a/src/Services/RateLimit.php b/src/Services/RateLimit.php index 4915ad2203..adacb00396 100644 --- a/src/Services/RateLimit.php +++ b/src/Services/RateLimit.php @@ -11,15 +11,31 @@ final class RateLimit { - public static function checkIPLimit(string $request_ip): bool + public function checkRateLimit(string $limit_type, string $value): bool { - $ip_limiter = new RedisRateLimiter( - Rate::perMinute($_ENV['rate_limit_ip']), - (new Cache())->initRedis() - ); + $limiter = match ($limit_type) { + 'sub_ip' => $this->getSubIpLimiter(), + 'sub_token' => $this->getSubTokenLimiter(), + 'webapi_ip' => $this->getWebApiIpLimiter(), + 'webapi_key' => $this->getWebApiKeyLimiter(), + 'user_api_ip' => $this->getUserApiIpLimiter(), + 'user_api_key' => $this->getUserApiKeyLimiter(), + 'admin_api_ip' => $this->getAdminApiIpLimiter(), + 'admin_api_key' => $this->getAdminApiKeyLimiter(), + 'node_api_ip' => $this->getNodeApiIpLimiter(), + 'node_api_key' => $this->getNodeApiKeyLimiter(), + 'email_request_ip' => $this->getEmailIpLimiter(), + 'email_request_address' => $this->getEmailAddressLimiter(), + 'ticket' => $this->getTicketLimiter(), + default => null, + }; + + if ($limiter === null) { + return false; + } try { - $ip_limiter->limit($request_ip); + $limiter->limit($value); } catch (LimitExceeded $e) { return false; } @@ -27,115 +43,120 @@ public static function checkIPLimit(string $request_ip): bool return true; } - public static function checkSubLimit(string $sub_token): bool + public function getSubIpLimiter(): RedisRateLimiter { - $sub_limiter = new RedisRateLimiter( - Rate::perMinute($_ENV['rate_limit_sub']), - (new Cache())->initRedis() + return new RedisRateLimiter( + Rate::perMinute((int) $_ENV['rate_limit_sub_ip']), + (new Cache())->initRedis(), + 'sspanel_sub_ip:' ); - - try { - $sub_limiter->limit($sub_token); - } catch (LimitExceeded $e) { - return false; - } - - return true; } - public static function checkWebAPILimit(string $web_api_token): bool + public function getSubTokenLimiter(): RedisRateLimiter { - $webapi_limiter = new RedisRateLimiter( - Rate::perMinute($_ENV['rate_limit_webapi']), - (new Cache())->initRedis() + return new RedisRateLimiter( + Rate::perMinute((int) $_ENV['rate_limit_sub']), + (new Cache())->initRedis(), + 'sspanel_sub_token:' ); + } - try { - $webapi_limiter->limit($web_api_token); - } catch (LimitExceeded $e) { - return false; - } + public function getWebApiIpLimiter(): RedisRateLimiter + { + return new RedisRateLimiter( + Rate::perMinute((int) $_ENV['rate_limit_webapi_ip']), + (new Cache())->initRedis(), + 'sspanel_webapi_ip:' + ); + } - return true; + public function getWebApiKeyLimiter(): RedisRateLimiter + { + return new RedisRateLimiter( + Rate::perMinute((int) $_ENV['rate_limit_webapi']), + (new Cache())->initRedis(), + 'sspanel_webapi_key:' + ); } - public static function checkUserAPILimit(string $user_api_token): bool + public function getUserApiIpLimiter(): RedisRateLimiter { - $user_api_limiter = new RedisRateLimiter( - Rate::perMinute($_ENV['rate_limit_user_api']), - (new Cache())->initRedis() + return new RedisRateLimiter( + Rate::perMinute((int) $_ENV['rate_limit_user_api_ip']), + (new Cache())->initRedis(), + 'sspanel_user_api_ip:' ); + } - try { - $user_api_limiter->limit($user_api_token); - } catch (LimitExceeded $e) { - return false; - } + public function getUserApiKeyLimiter(): RedisRateLimiter + { + return new RedisRateLimiter( + Rate::perMinute((int) $_ENV['rate_limit_user_api']), + (new Cache())->initRedis(), + 'sspanel_user_api_key:' + ); + } - return true; + public function getAdminApiIpLimiter(): RedisRateLimiter + { + return new RedisRateLimiter( + Rate::perMinute((int) $_ENV['rate_limit_admin_api_ip']), + (new Cache())->initRedis(), + 'sspanel_admin_api_ip:' + ); } - public static function checkAdminAPILimit(string $admin_api_token): bool + public function getAdminApiKeyLimiter(): RedisRateLimiter { - $admin_api_limiter = new RedisRateLimiter( - Rate::perMinute($_ENV['rate_limit_admin_api']), - (new Cache())->initRedis() + return new RedisRateLimiter( + Rate::perMinute((int) $_ENV['rate_limit_admin_api']), + (new Cache())->initRedis(), + 'sspanel_admin_api_key:' ); + } - try { - $admin_api_limiter->limit($admin_api_token); - } catch (LimitExceeded $e) { - return false; - } + public function getNodeApiIpLimiter(): RedisRateLimiter + { + return new RedisRateLimiter( + Rate::perMinute((int) $_ENV['rate_limit_node_api_ip']), + (new Cache())->initRedis(), + 'sspanel_node_api_ip:' + ); + } - return true; + public function getNodeApiKeyLimiter(): RedisRateLimiter + { + return new RedisRateLimiter( + Rate::perMinute((int) $_ENV['rate_limit_node_api']), + (new Cache())->initRedis(), + 'sspanel_node_api_key:' + ); } - public static function checkEmailIpLimit(string $request_ip): bool + public function getEmailIpLimiter(): RedisRateLimiter { - $email_ip_limiter = new RedisRateLimiter( + return new RedisRateLimiter( Rate::perHour(Config::obtain('email_request_ip_limit')), - (new Cache())->initRedis() + (new Cache())->initRedis(), + 'sspanel_email_request_ip:' ); - - try { - $email_ip_limiter->limit($request_ip); - } catch (LimitExceeded $e) { - return false; - } - - return true; } - public static function checkEmailAddressLimit(string $request_address): bool + public function getEmailAddressLimiter(): RedisRateLimiter { - $email_address_limiter = new RedisRateLimiter( + return new RedisRateLimiter( Rate::perHour(Config::obtain('email_request_address_limit')), - (new Cache())->initRedis() + (new Cache())->initRedis(), + 'sspanel_email_request_address:' ); - - try { - $email_address_limiter->limit($request_address); - } catch (LimitExceeded $e) { - return false; - } - - return true; } - public static function checkTicketLimit(int $user_id): bool + public function getTicketLimiter(): RedisRateLimiter { - $ticket_limiter = new RedisRateLimiter( + return new RedisRateLimiter( Rate::custom(Config::obtain('ticket_limit'), 2592000), - (new Cache())->initRedis() + (new Cache())->initRedis(), + 'sspanel_ticket:' ); - - try { - $ticket_limiter->limit((string) $user_id); - } catch (LimitExceeded $e) { - return false; - } - - return true; } }