redirect(login_landing()); } $error = ''; $blocked = false; $ip = $_SERVER['REMOTE_ADDR'] ?? ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 质量红线:登录必须校验 CSRF + 验证码 + IP 级双窗口限速(失败 10 分钟/5 次、成功 30 分钟/5 次),杜绝机器人暴力破解 if (!csrf_check()) { $error = '表单已过期,请刷新页面后重试'; } elseif (ip_login_blocked($ip)) { $error = '尝试次数过多,请 30 分钟后再试'; $blocked = true; } elseif (!captcha_check($this->post('captcha'))) { $error = '验证码错误,请重新计算'; } else { $u = trim($this->post('username')); $p = $this->post('password'); $ok = false; $user = null; $found = (new AdminUser())->byUsername($u); if ($found && (int)($found['status'] ?? 1) === 1 && password_verify($p, $found['password'])) { $ok = true; $user = $found; } elseif ($this->fallbackEnabled() && $u === \Core\App::config('admin.username') && $p === \Core\App::config('admin.password')) { // 配置文件兜底账号:仅本地/演示模式开启(本地规则要求生产禁用默认密码后门) $ok = true; $user = ['id' => 0, 'username' => $u, 'name' => '管理员', 'role' => 'super_admin']; } if ($ok) { ip_login_register_success($ip); // 记录成功登录(纳入 30 分钟 5 次上限),并重置失败计数 session_regenerate_id(true); // 防会话固定 $_SESSION['admin_logged'] = true; $_SESSION['admin_id'] = $user['id'] ?? 0; $_SESSION['admin_name'] = $user['name'] ?? $u; $_SESSION['admin_role'] = $user['role'] ?? 'super_admin'; $_SESSION['crm_role'] = $user['crm_role'] ?? 'none'; $_SESSION['psi_role'] = $user['psi_role'] ?? 'none'; $dec = function ($v) { $a = @json_decode((string)$v, true); return is_array($a) ? $a : null; }; $_SESSION['crm_perms'] = $dec($user['crm_perms'] ?? null); $_SESSION['psi_perms'] = $dec($user['psi_perms'] ?? null); $this->redirect(login_landing()); } ip_login_register_fail($ip); // 记录一次失败(纳入 10 分钟 5 次上限) $error = '用户名或密码错误'; } } // 被限速(IP 失败/成功过多)时返回 429 + Retry-After,明确告知客户端稍后再试 if ($blocked && !headers_sent()) { http_response_code(429); header('Retry-After: ' . ip_login_remaining($ip)); } $captcha = captcha_make(); // 每次渲染都发放新的算术验证码 return $this->view('admin/login', ['error' => $error, 'captcha' => $captcha]); } /** 配置文件兜底账号是否启用:生产(mysql 模式)默认关闭,本地演示可开 */ private function fallbackEnabled(): bool { return (bool) \Core\App::config('admin.allow_config_fallback', false) && \Core\App::config('app.driver', 'file') !== 'mysql'; } /** 修改当前登录账号的密码 */ public function password() { admin_required(); $error = ''; $ok = ''; $uid = admin_uid(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!csrf_check()) { $error = '表单已过期,请重试'; } else { $cur = $this->post('current_password'); $new = $this->post('new_password'); $confirm = $this->post('confirm_password'); $m = new AdminUser(); $me = $uid ? $m->find($uid) : null; if (!$me) { $error = '账号异常,请重新登录'; } elseif (!password_verify($cur, $me['password'])) { $error = '当前密码不正确'; } elseif (strlen($new) < 6) { $error = '新密码至少 6 位'; } elseif ($new !== $confirm) { $error = '两次输入的密码不一致'; } else { $m->update($uid, ['password' => password_hash($new, PASSWORD_DEFAULT)]); $ok = '密码修改成功'; } } } return $this->view('admin/password', ['error' => $error, 'ok' => $ok]); } public function logout() { unset($_SESSION['admin_logged'], $_SESSION['admin_name'], $_SESSION['admin_id'], $_SESSION['admin_role'], $_SESSION['crm_role'], $_SESSION['psi_role'], $_SESSION['crm_perms'], $_SESSION['psi_perms']); $this->redirect('admin/login'); } }