Browse Source

feat: use redis for temp data

M1Screw 2 years ago
parent
commit
3c44134498

+ 1 - 0
README.md

@@ -39,6 +39,7 @@ SSPanel UIM 的需要以下程序才能正常的安装和运行:
 - Nginx(必须使用 HTTPS/HTTPS is REQUIRED)
 - PHP 8.1+ (推荐开启 OPcache/OPcache is recommended)
 - MariaDB 10.6+(关闭严格模式,不兼容 MySQL/Disable strict mode, DO NOT USE MYSQL)
+- Redis 7.0+
 
 我们推荐用户在开始使用之前至少有一定程度的 PHP 和 Linux 使用知识,能够至少正确识别使用中所出现的问题并在 issue 中提供所需的信息。
 

+ 42 - 32
config/settings.json

@@ -351,14 +351,54 @@
     },
     {
         "id": null,
-        "item": "mail_driver",
+        "item": "email_driver",
         "value": "none",
-        "class": "mail",
+        "class": "email",
         "is_public": 0,
         "type": "string",
         "default": "none",
         "mark": "邮件服务提供商"
     },
+    {
+        "id": null,
+        "item": "email_verify_code_ttl",
+        "value": "3600",
+        "class": "email",
+        "is_public": 0,
+        "type": "int",
+        "default": "3600",
+        "mark": "邮箱验证码有效期"
+    },
+    {
+        "id": null,
+        "item": "email_password_reset_ttl",
+        "value": "3600",
+        "class": "email",
+        "is_public": 0,
+        "type": "int",
+        "default": "3600",
+        "mark": "邮箱重设密码链接有效期"
+    },
+    {
+        "id": null,
+        "item": "email_request_ip_limit",
+        "value": "3",
+        "class": "email",
+        "is_public": 0,
+        "type": "int",
+        "default": "3",
+        "mark": "单个IP每小时可请求的发信次数"
+    },
+    {
+        "id": null,
+        "item": "email_request_address_limit",
+        "value": "3",
+        "class": "email",
+        "is_public": 0,
+        "type": "int",
+        "default": "3",
+        "mark": "单个邮箱地址每小时可请求的发信次数"
+    },
     {
         "id": null,
         "item": "captcha_provider",
@@ -689,36 +729,6 @@
         "default": "0",
         "mark": "邮箱验证"
     },
-    {
-        "id": null,
-        "item": "email_verify_ttl",
-        "value": "3600",
-        "class": "register",
-        "is_public": 0,
-        "type": "int",
-        "default": "3600",
-        "mark": "邮箱验证码有效期"
-    },
-    {
-        "id": null,
-        "item": "email_verify_ip_limit",
-        "value": "5",
-        "class": "register",
-        "is_public": 0,
-        "type": "int",
-        "default": "5",
-        "mark": "验证码有效期内单个ip可请求的发信次数"
-    },
-    {
-        "id": null,
-        "item": "email_verify_email_limit",
-        "value": "5",
-        "class": "register",
-        "is_public": 0,
-        "type": "int",
-        "default": "5",
-        "mark": "验证码有效期内单个邮箱可请求的发信次数"
-    },
     {
         "id": null,
         "item": "enable_reg_im",

+ 0 - 29
db/migrations/2023020100-init.php

@@ -82,15 +82,6 @@ return new class() implements MigrationInterface {
                 PRIMARY KEY (`id`)
             ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
 
-            CREATE TABLE `email_verify` (
-                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '记录ID',
-                `email` varchar(255) NOT NULL DEFAULT '' COMMENT '邮箱',
-                `ip` varchar(255) NOT NULL DEFAULT '' COMMENT 'IP',
-                `code` varchar(255) NOT NULL DEFAULT '' COMMENT '验证码',
-                `expire_in` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '过期时间',
-                PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
             CREATE TABLE `gift_card` (
                 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '礼品卡ID',
                 `card` text NOT NULL DEFAULT '' COMMENT '卡号',
@@ -251,15 +242,6 @@ return new class() implements MigrationInterface {
                 PRIMARY KEY (`id`)
             ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
 
-            CREATE TABLE `telegram_session` (
-                `id` bigint(20) NOT NULL AUTO_INCREMENT,
-                `user_id` bigint(20) DEFAULT NULL,
-                `type` int(11) DEFAULT NULL,
-                `session_content` varchar(255) DEFAULT NULL,
-                `datetime` bigint(20) DEFAULT NULL,
-                PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
             CREATE TABLE `ticket` (
                 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '工单ID',
                 `title` varchar(255) NOT NULL DEFAULT '' COMMENT '工单标题',
@@ -382,17 +364,6 @@ return new class() implements MigrationInterface {
                 KEY `user_id` (`user_id`)
             ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
 
-            CREATE TABLE `user_password_reset` (
-                `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '记录ID',
-                `email` varchar(255) NOT NULL DEFAULT '' COMMENT '用户邮箱',
-                `token` varchar(255) NOT NULL DEFAULT '' COMMENT '重置密码的 token',
-                `init_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间',
-                `expire_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '过期时间',
-                PRIMARY KEY (`id`),
-                KEY `email` (`email`),
-                KEY `token` (`token`)
-            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
             CREATE TABLE `user_subscribe_log` (
                 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '记录ID',
                 `user_name` varchar(255) NOT NULL DEFAULT '' COMMENT '用户名',

+ 1 - 1
db/migrations/2023021600-drop_user_token.php

@@ -16,7 +16,7 @@ return new class() implements MigrationInterface {
     public function down(): int
     {
         DB::getPdo()->exec(
-            "CREATE TABLE `user_token` (
+            "CREATE TABLE IF NOT EXISTS `user_token` (
                 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
                 `token` varchar(255) DEFAULT NULL,
                 `user_id` bigint(20) unsigned DEFAULT NULL,

+ 55 - 0
db/migrations/2023071000-drop_temp_tables.php

@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+use App\Interfaces\MigrationInterface;
+use App\Services\DB;
+
+return new class() implements MigrationInterface {
+    public function up(): int
+    {
+        DB::getPdo()->exec('
+            DROP TABLE IF EXISTS `email_verify`;
+            DROP TABLE IF EXISTS `user_password_reset`;
+            DROP TABLE IF EXISTS `telegram_session`;
+        ');
+
+        return 2023071000;
+    }
+
+    public function down(): int
+    {
+        DB::getPdo()->exec(
+            "CREATE TABLE IF NOT EXISTS `email_verify` (
+                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '记录ID',
+                `email` varchar(255) NOT NULL DEFAULT '' COMMENT '邮箱',
+                `ip` varchar(255) NOT NULL DEFAULT '' COMMENT 'IP',
+                `code` varchar(255) NOT NULL DEFAULT '' COMMENT '验证码',
+                `expire_in` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '过期时间',
+                PRIMARY KEY (`id`)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+            
+            CREATE TABLE IF NOT EXISTS `telegram_session` (
+                `id` bigint(20) NOT NULL AUTO_INCREMENT,
+                `user_id` bigint(20) DEFAULT NULL,
+                `type` int(11) DEFAULT NULL,
+                `session_content` varchar(255) DEFAULT NULL,
+                `datetime` bigint(20) DEFAULT NULL,
+                PRIMARY KEY (`id`)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+            CREATE TABLE IF NOT EXISTS `user_password_reset` (
+                `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '记录ID',
+                `email` varchar(255) NOT NULL DEFAULT '' COMMENT '用户邮箱',
+                `token` varchar(255) NOT NULL DEFAULT '' COMMENT '重置密码的 token',
+                `init_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间',
+                `expire_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '过期时间',
+                PRIMARY KEY (`id`),
+                KEY `email` (`email`),
+                KEY `token` (`token`)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
+        );
+
+        return 2023063000;
+    }
+};

+ 42 - 7
resources/views/tabler/admin/setting/email.tpl

@@ -33,6 +33,9 @@
                         <li class="nav-item">
                             <a href="#email" class="nav-link active" data-bs-toggle="tab">邮件设置</a>
                         </li>
+                        <li class="nav-item">
+                            <a href="#limit" class="nav-link" data-bs-toggle="tab">发送限制</a>
+                        </li>
                         <li class="nav-item">
                             <a href="#smtp" class="nav-link" data-bs-toggle="tab">SMTP</a>
                         </li>
@@ -57,13 +60,13 @@
                                 <div class="form-group mb-3 row">
                                     <label class="form-label col-3 col-form-label">邮件服务提供商</label>
                                     <div class="col">
-                                        <select id="mail_driver" class="col form-select" value="{$settings['mail_driver']}">
-                                            <option value="none" {if $settings['mail_driver'] === "none"}selected{/if}>none</option>
-                                            <option value="smtp" {if $settings['mail_driver'] === "smtp"}selected{/if}>smtp</option>
-                                            <option value="sendgrid" {if $settings['mail_driver'] === "sendgrid"}selected{/if}>sendgrid</option>
-                                            <option value="mailgun" {if $settings['mail_driver'] === "mailgun"}selected{/if}>mailgun</option>
-                                            <option value="postal" {if $settings['mail_driver'] === "postal"}selected{/if}>postal</option>
-                                            <option value="ses" {if $settings['mail_driver'] === "ses"}selected{/if}>ses</option>
+                                        <select id="email_driver" class="col form-select" value="{$settings['email_driver']}">
+                                            <option value="none" {if $settings['email_driver'] === "none"}selected{/if}>none</option>
+                                            <option value="smtp" {if $settings['email_driver'] === "smtp"}selected{/if}>smtp</option>
+                                            <option value="sendgrid" {if $settings['email_driver'] === "sendgrid"}selected{/if}>sendgrid</option>
+                                            <option value="mailgun" {if $settings['email_driver'] === "mailgun"}selected{/if}>mailgun</option>
+                                            <option value="postal" {if $settings['email_driver'] === "postal"}selected{/if}>postal</option>
+                                            <option value="ses" {if $settings['email_driver'] === "ses"}selected{/if}>ses</option>
                                         </select>
                                     </div>
                                 </div>
@@ -78,6 +81,38 @@
                                 </div>
                             </div>
                         </div>
+                        <div class="tab-pane" id="limit">
+                            <div class="card-body">
+                                <div class="form-group mb-3 row">
+                                    <label class="form-label col-3 col-form-label">邮箱验证码有效期(秒)</label>
+                                    <div class="col">
+                                        <input id="email_verify_code_ttl" type="text" class="form-control"
+                                               value="{$settings['email_verify_code_ttl']}">
+                                    </div>
+                                </div>
+                                <div class="form-group mb-3 row">
+                                    <label class="form-label col-3 col-form-label">邮箱重设密码链接有效期(秒)</label>
+                                    <div class="col">
+                                        <input id="email_password_reset_ttl" type="text" class="form-control"
+                                               value="{$settings['email_password_reset_ttl']}">
+                                    </div>
+                                </div>
+                                <div class="form-group mb-3 row">
+                                    <label class="form-label col-3 col-form-label">单个IP每小时可请求的发信次数</label>
+                                    <div class="col">
+                                        <input id="email_request_ip_limit" type="text" class="form-control"
+                                               value="{$settings['email_request_ip_limit']}">
+                                    </div>
+                                </div>
+                                <div class="form-group mb-3 row">
+                                    <label class="form-label col-3 col-form-label">单个邮箱地址每小时可请求的发信次数</label>
+                                    <div class="col">
+                                        <input id="email_request_address_limit" type="text" class="form-control"
+                                               value="{$settings['email_request_address_limit']}">
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
                         <div class="tab-pane" id="smtp">
                             <div class="card-body">
                                 <div class="form-group mb-3 row">

+ 1 - 19
resources/views/tabler/admin/setting/reg.tpl

@@ -64,24 +64,6 @@
                                         </select>
                                     </div>
                                 </div>
-                                <div class="form-group mb-3 row">
-                                    <label class="form-label col-3 col-form-label">邮箱验证码有效期(秒)</label>
-                                    <div class="col">
-                                        <input id="email_verify_ttl" type="text" class="form-control" value="{$settings['email_verify_ttl']}">
-                                    </div>
-                                </div>
-                                <div class="form-group mb-3 row">
-                                    <label class="form-label col-3 col-form-label">验证码有效期内单个 IP 可请求次数</label>
-                                    <div class="col">
-                                        <input id="email_verify_ip_limit" type="text" class="form-control" value="{$settings['email_verify_ip_limit']}">
-                                    </div>
-                                </div>
-                                <div class="form-group mb-3 row">
-                                    <label class="form-label col-3 col-form-label">验证码有效期内单个邮箱地址可请求次数</label>
-                                    <div class="col">
-                                        <input id="email_verify_email_limit" type="text" class="form-control" value="{$settings['email_verify_email_limit']}">
-                                    </div>
-                                </div>
                                 <div class="form-group mb-3 row">
                                     <label class="form-label col-3 col-form-label">默认接收每日用量邮件推送</label>
                                     <div class="col">
@@ -231,4 +213,4 @@
     });
 </script>
 
-{include file='admin/footer.tpl'}
+{include file='admin/footer.tpl'}

+ 3 - 3
resources/views/tabler/password/reset.tpl

@@ -42,9 +42,9 @@
                     </div>
                 </div>
             </div>
-        </div>
-        <div class="text-center text-secondary mt-3">
-            已有账户? <a href="/auth/login" tabindex="-1">点击登录</a>
+            <div class="text-center text-secondary mt-3">
+                已有账户? <a href="/auth/login" tabindex="-1">点击登录</a>
+            </div>
         </div>
     </div>
 

+ 3 - 3
resources/views/tabler/password/token.tpl

@@ -30,9 +30,9 @@
                     </div>
                 </div>
             </div>
-        </div>
-        <div class="text-center text-secondary mt-3">
-            已有账户? <a href="/auth/login" tabindex="-1">点击登录</a>
+            <div class="text-center text-secondary mt-3">
+                已有账户? <a href="/auth/login" tabindex="-1">点击登录</a>
+            </div>
         </div>
     </div>
 

+ 5 - 1
src/Controllers/Admin/Setting/EmailController.php

@@ -14,7 +14,11 @@ use function json_encode;
 final class EmailController extends BaseController
 {
     public static array $update_field = [
-        'mail_driver',
+        'email_driver',
+        'email_verify_code_ttl',
+        'email_password_reset_ttl',
+        'email_request_ip_limit',
+        'email_request_address_limit',
         // SMTP
         'smtp_host',
         'smtp_username',

+ 0 - 3
src/Controllers/Admin/Setting/RegController.php

@@ -14,9 +14,6 @@ final class RegController extends BaseController
     public static array $update_field = [
         'reg_mode',
         'reg_email_verify',
-        'email_verify_ttl',
-        'email_verify_ip_limit',
-        'email_verify_email_limit',
         'sign_up_for_daily_report',
         'enable_reg_im',
         'random_group',

+ 24 - 32
src/Controllers/AuthController.php

@@ -4,13 +4,14 @@ declare(strict_types=1);
 
 namespace App\Controllers;
 
-use App\Models\EmailVerify;
 use App\Models\InviteCode;
 use App\Models\Setting;
 use App\Models\User;
 use App\Services\Auth;
+use App\Services\Cache;
 use App\Services\Captcha;
 use App\Services\Mail;
+use App\Services\RateLimit;
 use App\Utils\Cookie;
 use App\Utils\Hash;
 use App\Utils\ResponseHelper;
@@ -19,6 +20,7 @@ use Exception;
 use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Message\ResponseInterface;
 use Ramsey\Uuid\Uuid;
+use RedisException;
 use Slim\Http\Response;
 use Slim\Http\ServerRequest;
 use Vectorface\GoogleAuthenticator;
@@ -156,11 +158,13 @@ final class AuthController extends BaseController
             ->fetch('auth/register.tpl'));
     }
 
+    /**
+     * @throws RedisException
+     */
     public function sendVerify(ServerRequest $request, Response $response, $next): Response|ResponseInterface
     {
         if (Setting::obtain('reg_email_verify')) {
             $antiXss = new AntiXSS();
-
             $email = strtolower(trim($antiXss->xss_clean($request->getParam('email'))));
 
             if ($email === '') {
@@ -173,32 +177,21 @@ final class AuthController extends BaseController
                 return $response->withJson($check_res);
             }
 
-            $user = User::where('email', $email)->first();
-            if ($user !== null) {
-                return ResponseHelper::error($response, '此邮箱已经注册');
+            if (! RateLimit::checkEmailIpLimit($request->getServerParam('REMOTE_ADDR')) ||
+                ! RateLimit::checkEmailAddressLimit($email)
+            ) {
+                return ResponseHelper::error($response, '你的请求过于频繁,请稍后再试');
             }
 
-            $ipcount = EmailVerify::where('ip', '=', $_SERVER['REMOTE_ADDR'])
-                ->where('expire_in', '>', time())
-                ->count();
-            if ($ipcount > Setting::obtain('email_verify_ip_limit')) {
-                return ResponseHelper::error($response, '此IP请求次数过多');
-            }
+            $user = User::where('email', $email)->first();
 
-            $mailcount = EmailVerify::where('email', '=', $email)
-                ->where('expire_in', '>', time())
-                ->count();
-            if ($mailcount > Setting::obtain('email_verify_email_limit')) {
-                return ResponseHelper::error($response, '此邮箱请求次数过多');
+            if ($user !== null) {
+                return ResponseHelper::error($response, '此邮箱已经注册');
             }
 
             $code = Tools::genRandomChar(6);
-            $ev = new EmailVerify();
-            $ev->expire_in = time() + Setting::obtain('email_verify_ttl');
-            $ev->ip = $_SERVER['REMOTE_ADDR'];
-            $ev->email = $email;
-            $ev->code = $code;
-            $ev->save();
+            $redis = Cache::initRedis();
+            $redis->setex($code, Setting::obtain('email_verify_code_ttl'), $email);
 
             try {
                 Mail::send(
@@ -207,16 +200,17 @@ final class AuthController extends BaseController
                     'verify_code.tpl',
                     [
                         'code' => $code,
-                        'expire' => date('Y-m-d H:i:s', time() + Setting::obtain('email_verify_ttl')),
+                        'expire' => date('Y-m-d H:i:s', time() + Setting::obtain('email_verify_code_ttl')),
                     ]
                 );
-            } catch (Exception | ClientExceptionInterface $e) {
+            } catch (Exception|ClientExceptionInterface $e) {
                 return ResponseHelper::error($response, '邮件发送失败,请联系网站管理员。');
             }
 
             return ResponseHelper::successfully($response, '验证码发送成功,请查收邮件。');
         }
-        return ResponseHelper::error($response, ' 不允许注册');
+
+        return ResponseHelper::error($response, '站点未启用邮件验证');
     }
 
     /**
@@ -412,17 +406,15 @@ final class AuthController extends BaseController
         }
 
         if (Setting::obtain('reg_email_verify')) {
-            $email_code = trim($antiXss->xss_clean($request->getParam('emailcode')));
-            $email_verify = EmailVerify::where('email', '=', $email)
-                ->where('code', '=', $email_code)
-                ->where('expire_in', '>', time())
-                ->first();
+            $redis = Cache::initRedis();
+            $email_verify_code = trim($antiXss->xss_clean($request->getParam('emailcode')));
+            $email_verify = $redis->get($email_verify_code);
 
-            if ($email_verify === null) {
+            if (! $email_verify) {
                 return ResponseHelper::error($response, '你的邮箱验证码不正确');
             }
 
-            EmailVerify::where('email', $email)->delete();
+            $redis->del($email_verify_code);
         }
 
         return $this->registerHelper($response, $name, $email, $passwd, $code, $imtype, $imvalue, 0, 0, 0);

+ 40 - 19
src/Controllers/PasswordController.php

@@ -4,20 +4,22 @@ declare(strict_types=1);
 
 namespace App\Controllers;
 
-use App\Models\PasswordReset;
 use App\Models\Setting;
 use App\Models\User;
+use App\Services\Cache;
 use App\Services\Captcha;
 use App\Services\Password;
+use App\Services\RateLimit;
 use App\Utils\Hash;
 use App\Utils\ResponseHelper;
 use Exception;
 use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Message\ResponseInterface;
+use RedisException;
 use Slim\Http\Response;
 use Slim\Http\ServerRequest;
+use voku\helper\AntiXSS;
 use function strlen;
-use function time;
 
 /*
  * Class Password
@@ -45,6 +47,9 @@ final class PasswordController extends BaseController
         );
     }
 
+    /**
+     * @throws RedisException
+     */
     public function handleReset(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
         if (Setting::obtain('enable_reset_password_captcha')) {
@@ -54,14 +59,26 @@ final class PasswordController extends BaseController
             }
         }
 
-        $email = strtolower($request->getParam('email'));
+        $antiXss = new AntiXSS();
+        $email = strtolower($antiXss->xss_clean($request->getParam('email')));
+
+        if ($email === '') {
+            return ResponseHelper::error($response, '未填写邮箱');
+        }
+
+        if (! RateLimit::checkEmailIpLimit($request->getServerParam('REMOTE_ADDR')) ||
+            ! RateLimit::checkEmailAddressLimit($email)
+        ) {
+            return ResponseHelper::error($response, '你的请求过于频繁,请稍后再试');
+        }
+
         $user = User::where('email', $email)->first();
         $msg = '如果你的账户存在于我们的数据库中,那么重置密码的链接将会发送到你账户所对应的邮箱。';
 
         if ($user !== null) {
             try {
                 Password::sendResetEmail($email);
-            } catch (ClientExceptionInterface $e) {
+            } catch (ClientExceptionInterface|RedisException $e) {
                 $msg = '邮件发送失败,请联系网站管理员。';
             }
         }
@@ -74,10 +91,12 @@ final class PasswordController extends BaseController
      */
     public function token(ServerRequest $request, Response $response, array $args)
     {
-        $token = PasswordReset::where('token', $args['token'])
-            ->where('expire_time', '>', time())->orderBy('id', 'desc')->first();
+        $antiXss = new AntiXSS();
+        $token = $antiXss->xss_clean($args['token']);
+        $redis = Cache::initRedis();
+        $email = $redis->get($token);
 
-        if ($token === null) {
+        if (! $email) {
             return $response->withStatus(302)->withHeader('Location', '/password/reset');
         }
 
@@ -86,9 +105,13 @@ final class PasswordController extends BaseController
         );
     }
 
+    /**
+     * @throws RedisException
+     */
     public function handleToken(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
-        $tokenStr = $args['token'];
+        $antiXss = new AntiXSS();
+        $token = $antiXss->xss_clean($args['token']);
         $password = $request->getParam('password');
         $repasswd = $request->getParam('repasswd');
 
@@ -97,19 +120,19 @@ final class PasswordController extends BaseController
         }
 
         if (strlen($password) < 8) {
-            return ResponseHelper::error($response, '密码太短啦');
+            return ResponseHelper::error($response, '密码过短');
         }
 
-        /** @var PasswordReset $token */
-        $token = PasswordReset::where('token', $tokenStr)
-            ->where('expire_time', '>', time())->orderBy('id', 'desc')->first();
-        if ($token === null) {
-            return ResponseHelper::error($response, '链接已经失效,请重新获取');
+        $redis = Cache::initRedis();
+        $email = $redis->get($token);
+
+        if (! $email) {
+            return ResponseHelper::error($response, '链接无效');
         }
 
-        $user = $token->user();
+        $user = User::where('email', $email)->first();
         if ($user === null) {
-            return ResponseHelper::error($response, '链接已经失效,请重新获取');
+            return ResponseHelper::error($response, '链接无效');
         }
 
         // reset password
@@ -124,9 +147,7 @@ final class PasswordController extends BaseController
             $user->cleanLink();
         }
 
-        // 禁止链接多次使用
-        $token->expire_time = time();
-        $token->save();
+        $redis->del($token);
 
         return ResponseHelper::successfully($response, '重置成功');
     }

+ 10 - 8
src/Controllers/UserController.php

@@ -6,7 +6,6 @@ namespace App\Controllers;
 
 use App\Models\Ann;
 use App\Models\Docs;
-use App\Models\EmailVerify;
 use App\Models\InviteCode;
 use App\Models\LoginIp;
 use App\Models\Node;
@@ -16,6 +15,7 @@ use App\Models\Setting;
 use App\Models\StreamMedia;
 use App\Models\User;
 use App\Services\Auth;
+use App\Services\Cache;
 use App\Services\Captcha;
 use App\Services\Config;
 use App\Services\DB;
@@ -27,6 +27,7 @@ use App\Utils\Tools;
 use Exception;
 use Psr\Http\Message\ResponseInterface;
 use Ramsey\Uuid\Uuid;
+use RedisException;
 use Slim\Http\Response;
 use Slim\Http\ServerRequest;
 use voku\helper\AntiXSS;
@@ -262,6 +263,9 @@ final class UserController extends BaseController
         return ResponseHelper::successfully($response, '修改成功');
     }
 
+    /**
+     * @throws RedisException
+     */
     public function updateEmail(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
     {
         $antiXss = new AntiXSS();
@@ -292,17 +296,15 @@ final class UserController extends BaseController
         }
 
         if (Setting::obtain('reg_email_verify')) {
-            $email_code = $request->getParam('emailcode');
-            $email_verify = EmailVerify::where('email', '=', $new_email)
-                ->where('code', '=', $email_code)
-                ->where('expire_in', '>', time())
-                ->first();
+            $redis = Cache::initRedis();
+            $email_verify_code = $request->getParam('emailcode');
+            $email_verify = $redis->get($email_verify_code);
 
-            if ($email_verify === null) {
+            if (! $email_verify) {
                 return ResponseHelper::error($response, '你的邮箱验证码不正确');
             }
 
-            EmailVerify::where('email', $email)->delete();
+            $redis->del($email_verify_code);
         }
 
         $user->email = $new_email;

+ 1 - 1
src/Models/EmailQueue.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 namespace App\Models;
 
 /**
- * EmailVerify Model
+ * EmailQueue Model
  */
 final class EmailQueue extends Model
 {

+ 0 - 14
src/Models/EmailVerify.php

@@ -1,14 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace App\Models;
-
-/**
- * EmailVerify Model
- */
-final class EmailVerify extends Model
-{
-    protected $connection = 'default';
-    protected $table = 'email_verify';
-}

+ 0 - 19
src/Models/PasswordReset.php

@@ -1,19 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace App\Models;
-
-final class PasswordReset extends Model
-{
-    protected $connection = 'default';
-    protected $table = 'user_password_reset';
-
-    /**
-     * 获取对应用户
-     */
-    public function user(): ?User
-    {
-        return User::where('email', $this->email)->first();
-    }
-}

+ 0 - 14
src/Models/TelegramSession.php

@@ -1,14 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace App\Models;
-
-/**
- * TelegramSession Model
- */
-final class TelegramSession extends Model
-{
-    protected $connection = 'default';
-    protected $table = 'telegram_session';
-}

+ 0 - 3
src/Models/User.php

@@ -384,13 +384,10 @@ final class User extends Model
 
         DetectBanLog::where('user_id', '=', $uid)->delete();
         DetectLog::where('user_id', '=', $uid)->delete();
-        EmailVerify::where('email', $email)->delete();
         InviteCode::where('user_id', '=', $uid)->delete();
         OnlineLog::where('user_id', '=', $uid)->delete();
         Link::where('userid', '=', $uid)->delete();
         LoginIp::where('userid', '=', $uid)->delete();
-        PasswordReset::where('email', '=', $email)->delete();
-        TelegramSession::where('user_id', '=', $uid)->delete();
         UserSubscribeLog::where('user_id', '=', $uid)->delete();
 
         $this->delete();

+ 0 - 4
src/Services/CronJob.php

@@ -11,11 +11,9 @@ use App\Models\Invoice;
 use App\Models\Node;
 use App\Models\OnlineLog;
 use App\Models\Order;
-use App\Models\PasswordReset;
 use App\Models\Paylist;
 use App\Models\Setting;
 use App\Models\StreamMedia;
-use App\Models\TelegramSession;
 use App\Models\User;
 use App\Models\UserHourlyUsage;
 use App\Models\UserSubscribeLog;
@@ -69,10 +67,8 @@ final class CronJob
         UserHourlyUsage::where('datetime', '<', time() - 86400 * (int) $_ENV['trafficLog_keep_days'])->delete();
         DetectLog::where('datetime', '<', time() - 86400 * 3)->delete();
         EmailQueue::where('time', '<', time() - 86400)->delete();
-        PasswordReset::where('expire_time', '<', time() - 86400)->delete();
         OnlineLog::where('last_time', '<', time() - 86400)->delete();
         StreamMedia::where('created_at', '<', time() - 86400)->delete();
-        TelegramSession::where('datetime', '<', time() - 900)->delete();
 
         echo date('Y-m-d H:i:s') . ' 数据库清理完成' . PHP_EOL;
     }

+ 1 - 1
src/Services/Mail.php

@@ -23,7 +23,7 @@ final class Mail
 {
     public static function getClient(): Mailgun|Smtp|SendGrid|NullMail|Ses|Postal
     {
-        $driver = Setting::obtain('mail_driver');
+        $driver = Setting::obtain('email_driver');
         return match ($driver) {
             'mailgun' => new Mailgun(),
             'ses' => new Ses(),

+ 8 - 9
src/Services/Password.php

@@ -4,27 +4,26 @@ declare(strict_types=1);
 
 namespace App\Services;
 
-use App\Models\PasswordReset;
+use App\Models\Setting;
 use App\Utils\Tools;
 use Psr\Http\Client\ClientExceptionInterface;
-use function time;
+use RedisException;
 
 final class Password
 {
     /**
      * @throws ClientExceptionInterface
+     * @throws RedisException
      */
     public static function sendResetEmail($email): void
     {
-        $pwdRst = new PasswordReset();
-        $pwdRst->email = $email;
-        $pwdRst->init_time = time();
-        $pwdRst->expire_time = time() + 3600 * 24;
-        $pwdRst->token = Tools::genRandomChar(64);
-        $pwdRst->save();
+        $redis = Cache::initRedis();
+        $token = Tools::genRandomChar(64);
+
+        $redis->setex($token, Setting::obtain('email_password_reset_ttl'), $email);
 
         $subject = $_ENV['appName'] . '-重置密码';
-        $resetUrl = $_ENV['baseUrl'] . '/password/token/' . $pwdRst->token;
+        $resetUrl = $_ENV['baseUrl'] . '/password/token/' . $token;
 
         Mail::send(
             $email,

+ 39 - 0
src/Services/RateLimit.php

@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace App\Services;
 
+use App\Models\Setting;
 use RateLimit\Exception\LimitExceeded;
 use RateLimit\Rate;
 use RateLimit\RedisRateLimiter;
@@ -105,4 +106,42 @@ final class RateLimit
 
         return true;
     }
+
+    /**
+     * @throws RedisException
+     */
+    public static function checkEmailIpLimit(string $request_ip): bool
+    {
+        $email_ip_limiter = new RedisRateLimiter(
+            Rate::perHour(Setting::obtain('email_request_ip_limit')),
+            Cache::initRedis()
+        );
+
+        try {
+            $email_ip_limiter->limit($request_ip);
+        } catch (LimitExceeded $e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @throws RedisException
+     */
+    public static function checkEmailAddressLimit(string $request_address): bool
+    {
+        $email_address_limiter = new RedisRateLimiter(
+            Rate::perHour(Setting::obtain('email_request_address_limit')),
+            Cache::initRedis()
+        );
+
+        try {
+            $email_address_limiter->limit($request_address);
+        } catch (LimitExceeded $e) {
+            return false;
+        }
+
+        return true;
+    }
 }

+ 26 - 29
src/Utils/Telegram.php

@@ -4,12 +4,13 @@ declare(strict_types=1);
 
 namespace App\Utils;
 
-use App\Models\TelegramSession;
+use App\Services\Cache;
 use Exception;
+use RedisException;
 use Telegram\Bot\Api;
 use Telegram\Bot\Exceptions\TelegramSDKException;
+use voku\helper\AntiXSS;
 use function strip_tags;
-use function time;
 
 final class Telegram
 {
@@ -152,45 +153,41 @@ final class Telegram
 
     public static function generateRandomLink(): string
     {
-        for ($i = 0; $i < 10; $i++) {
-            $token = Tools::genRandomChar(16);
-            $session = TelegramSession::where('session_content', '=', $token)->first();
-
-            if ($session === null) {
-                return $token;
-            }
-        }
-
-        return "couldn't alloc token";
+        return Tools::genRandomChar(16);
     }
 
+    /**
+     * @throws RedisException
+     */
     public static function verifyBindSession($token): int
     {
-        $session = TelegramSession::where('type', '=', 0)->where('session_content', $token)
-            ->where('datetime', '>', time() - 600)->orderBy('datetime', 'desc')->first();
+        $antiXss = new AntiXSS();
+        $redis = Cache::initRedis();
+        $uid = $redis->get($antiXss->xss_clean($token));
 
-        if ($session !== null) {
-            $uid = $session->user_id;
-            $session->delete();
-            return $uid;
+        if (! $uid) {
+            return 0;
         }
 
-        return 0;
+        $redis->del($token);
+
+        return (int) $uid;
     }
 
+    /**
+     * @throws RedisException
+     */
     public static function addBindSession($user): string
     {
-        $session = TelegramSession::where('type', '=', 0)->where('user_id', '=', $user->id)->first();
+        $redis = Cache::initRedis();
+        $token = self::generateRandomLink();
 
-        if ($session === null) {
-            $session = new TelegramSession();
-            $session->type = 0;
-            $session->user_id = $user->id;
-        }
+        $redis->setex(
+            $token,
+            600,
+            $user->id
+        );
 
-        $session->datetime = time();
-        $session->session_content = self::generateRandomLink();
-        $session->save();
-        return $session->session_content;
+        return $token;
     }
 }

+ 4 - 0
src/Utils/Telegram/Commands/StartCommand.php

@@ -8,6 +8,7 @@ use App\Models\Setting;
 use App\Models\User;
 use App\Utils\Telegram;
 use App\Utils\Telegram\TelegramTools;
+use RedisException;
 use Telegram\Bot\Actions;
 use Telegram\Bot\Commands\Command;
 use function strlen;
@@ -81,6 +82,9 @@ final class StartCommand extends Command
         }
     }
 
+    /**
+     * @throws RedisException
+     */
     public function bindingAccount($SendUser, $MessageText): void
     {
         $Uid = Telegram::verifyBindSession($MessageText);

+ 7 - 1
src/Utils/Telegram/Message.php

@@ -6,6 +6,7 @@ namespace App\Utils\Telegram;
 
 use App\Models\Setting;
 use App\Utils\Telegram;
+use RedisException;
 use Telegram\Bot\Api;
 use Telegram\Bot\Exceptions\TelegramSDKException;
 use function count;
@@ -43,6 +44,7 @@ final class Message
 
     /**
      * @throws TelegramSDKException
+     * @throws RedisException
      */
     public function __construct(Api $bot, \Telegram\Bot\Objects\Message $Message)
     {
@@ -70,7 +72,11 @@ final class Message
                     $BinsUser = TelegramTools::getUser($Uid, 'id');
                     $BinsUser->telegram_id = $this->triggerUser['id'];
                     $BinsUser->im_type = 4;
-                    $BinsUser->im_value = $this->triggerUser['username'];
+                    if ($this->triggerUser['username'] === null) {
+                        $BinsUser->im_value = '用戶名未设置';
+                    } else {
+                        $BinsUser->im_value = $this->triggerUser['username'];
+                    }
                     $BinsUser->save();
                     if ($BinsUser->is_admin === 1) {
                         $text = '尊敬的**管理员**你好,恭喜绑定成功。' . PHP_EOL . '当前绑定邮箱为:' . $BinsUser->email;