## 管理员账号(用户名 + 口令) 原先只有口令,没有用户名。现在两者都有: - 用户名词入库 settings 的 admin.username(2~32 位,字母数字下划线点连字符) - 口令仍是 bcrypt 哈希存 admin.password_hash,明文不落任何文件 - 登录校验 fb_admin_verify_login():用户名或口令任一不对都返回同一句 「用户名或口令不正确」,且用户名错时也照常跑一次 password_verify,避免用耗时探测账号 - 向后兼容:老部署只设了口令没用户名时,仍可只填口令登录, 并在登录页提示补一个用户名 ## 安装向导精简 按用户要求「没数据就初始化,填完进主页」: - 必填只剩 4 项:网站名称、管理员用户名、口令、确认口令 - 站点描述 / 官方邮箱 / 站点根地址改为 hidden(可留空自动探测) - 密钥、令牌原本就自动生成 - 安装成功后展示网站名称与用户名,2 秒自动跳首页,也有按钮 - 按钮文案改为「完成安装,进入首页」 ## 修掉一个真实 bug 首页把软件说明当成站点描述,导致「软件有 note 时站点描述被顶掉」。 两者是不同的东西:站点描述是全局设置必须始终显示,软件说明另起一行补充。 老基线 D6「首页描述变为新描述」就是被这个 bug 弄失败的。 ## 另一个坑 admin.username 忘了加进 fb_setting_keys() 白名单,fb_set_setting() 会静默丢弃, 表现为「安装时填了用户名,装完却没有」。已补。 验证: - 全新安装流程 32/32(真空站:无 config.php、无 data/*.db) - 老基线 25/25、附件专项 22/22、安装向导 60/60 - PHP 8.5 全部入口 0 弃用
This commit is contained in:
@@ -25,21 +25,26 @@ if (isset($_GET['logout'])) {
|
||||
/* ---------------- 登录 ---------------- */
|
||||
$loginErr = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
|
||||
$pw = isset($_POST['admin_password']) ? (string)$_POST['admin_password'] : '';
|
||||
$cs = isset($_POST['csrf']) ? (string)$_POST['csrf'] : '';
|
||||
$user = isset($_POST['admin_user']) ? (string)$_POST['admin_user'] : '';
|
||||
$pw = isset($_POST['admin_password']) ? (string)$_POST['admin_password'] : '';
|
||||
$cs = isset($_POST['csrf']) ? (string)$_POST['csrf'] : '';
|
||||
if (!fb_csrf_ok($cs)) {
|
||||
$loginErr = '会话已过期,请重试。';
|
||||
} elseif (!fb_rate_check('admin_login', 10)) {
|
||||
$loginErr = '尝试次数过多,请一小时后再试。';
|
||||
} elseif (!fb_admin_password_is_set()) {
|
||||
$loginErr = '本系统还没有设置登录口令,请先运行安装向导(install.php)完成设置。';
|
||||
} elseif ($pw !== '' && fb_admin_verify_password($pw)) {
|
||||
fb_admin_login();
|
||||
fb_mod_log('login');
|
||||
fb_redirect('admin.php');
|
||||
} else {
|
||||
fb_rate_hit('admin_login');
|
||||
$loginErr = '口令不正确。';
|
||||
$rc = fb_admin_verify_login($user, $pw);
|
||||
if ($rc === 0) {
|
||||
fb_admin_login();
|
||||
fb_mod_log('login');
|
||||
fb_redirect('admin.php');
|
||||
} elseif ($rc === 2) {
|
||||
$loginErr = '本系统还没有配置管理员账号,请先运行安装向导(install.php)完成设置。';
|
||||
} else {
|
||||
fb_rate_hit('admin_login');
|
||||
// 不区分「用户名错」还是「口令错」,避免被用来探测账号
|
||||
$loginErr = '用户名或口令不正确。';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,13 +54,21 @@ if (!fb_admin_logged_in()) {
|
||||
<div class="crumbs"><a href="index.php">← 返回问题列表</a></div>
|
||||
<section class="narrow">
|
||||
<h1>管理登录</h1>
|
||||
<p class="muted">请输入管理员口令<?php if (fb_admin_password_source() === 'file'): ?>(当前仍是 <code>config.php</code> 里的 <code>admin_password</code> 明文,登录后请到「站点设置」改成加密存储)<?php endif; ?>。</p>
|
||||
<p class="muted">请输入管理员用户名与口令<?php if (fb_admin_password_source() === 'file'): ?>(当前口令仍是 <code>config.php</code> 里的明文,登录后请到「站点设置」改成加密存储)<?php endif; ?>。</p>
|
||||
<?php if (fb_admin_password_source() === 'none'): ?>
|
||||
<div class="flash error">尚未设置登录口令,且出厂占位口令已被禁用。请运行 <a href="install.php">install.php</a> 设定一个。</div>
|
||||
<div class="flash error">尚未配置管理员账号,且出厂占位口令已被禁用。请运行 <a href="install.php">install.php</a> 完成初始化。</div>
|
||||
<?php endif; ?>
|
||||
<?php if (fb_admin_username() === '' && fb_admin_password_is_set()): ?>
|
||||
<div class="flash error">本站只设了口令、没有用户名(旧版安装)。请运行 <a href="install.php">install.php</a> 补一个用户名;在那之前用户名可留空、只填口令。</div>
|
||||
<?php endif; ?>
|
||||
<?php fb_flash_box($loginErr, 'error'); ?>
|
||||
<form method="post" action="admin.php" class="card-form">
|
||||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||||
<label class="field">
|
||||
<span>管理员用户名</span>
|
||||
<input type="text" name="admin_user" autocomplete="username" required
|
||||
value="<?php echo e(isset($_POST['admin_user']) ? (string)$_POST['admin_user'] : ''); ?>">
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>管理员口令</span>
|
||||
<input type="password" name="admin_password" autocomplete="current-password" required>
|
||||
|
||||
@@ -83,10 +83,12 @@ fb_layout_head($sw ? ((string)$sw['name']) : $headTitle);
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1><?php echo e($sw ? (string)$sw['name'] : '全部问题'); ?></h1>
|
||||
<p class="muted"><?php
|
||||
$desc = $sw && !empty($sw['note']) ? (string)$sw['note'] : (string)fb_cfg('site_desc');
|
||||
echo e($desc);
|
||||
?></p>
|
||||
<?php
|
||||
$siteDesc = (string)fb_cfg('site_desc');
|
||||
$swNote = ($sw && !empty($sw['note'])) ? (string)$sw['note'] : '';
|
||||
?>
|
||||
<?php if ($siteDesc !== ''): ?><p class="muted"><?php echo e($siteDesc); ?></p><?php endif; ?>
|
||||
<?php if ($swNote !== ''): ?><p class="muted small" style="margin:2px 0 0"><?php echo e($swNote); ?></p><?php endif; ?>
|
||||
</div>
|
||||
<a class="btn primary" href="new.php<?php echo $sw ? '?sw=' . e(urlencode((string)$sw['slug'])) : ''; ?>">我要反馈</a>
|
||||
</div>
|
||||
|
||||
+61
-51
@@ -173,6 +173,7 @@ if (isset($_POST['do_install'])) {
|
||||
$siteDesc = $post('site_desc');
|
||||
$admEmail = $post('admin_email');
|
||||
$baseUrl = rtrim($post('base_url'), '/');
|
||||
$admUser = $post('admin_user');
|
||||
$pw = (string)(isset($_POST['admin_password']) ? $_POST['admin_password'] : '');
|
||||
$pw2 = (string)(isset($_POST['admin_password2']) ? $_POST['admin_password2'] : '');
|
||||
$setPw = !empty($_POST['set_password']);
|
||||
@@ -187,9 +188,13 @@ if (isset($_POST['do_install'])) {
|
||||
$err = '官方邮箱格式不正确(也可以留空)。';
|
||||
} elseif ($baseUrl !== '' && !preg_match('~^https?://~i', $baseUrl)) {
|
||||
$err = '站点根地址需要以 http:// 或 https:// 开头(留空则自动探测)。';
|
||||
} elseif ($setPw && fb_len($pw) < 8) {
|
||||
} elseif ($admUser === '') {
|
||||
$err = '请填写管理员用户名。';
|
||||
} elseif (!preg_match('/^[A-Za-z0-9_.\-]{2,32}$/', $admUser)) {
|
||||
$err = '管理员用户名只能用字母、数字、下划线、点、连字符,长度 2~32。';
|
||||
} elseif (fb_len($pw) < 8) {
|
||||
$err = '管理员口令至少 8 个字符。';
|
||||
} elseif ($setPw && $pw !== $pw2) {
|
||||
} elseif ($pw !== $pw2) {
|
||||
$err = '两次输入的口令不一致。';
|
||||
} else {
|
||||
// ① 先建库建表 —— 后面写设置才有地方写
|
||||
@@ -217,9 +222,10 @@ if (isset($_POST['do_install'])) {
|
||||
if ($siteDesc !== '') { fb_set_setting('site_desc', $siteDesc); }
|
||||
fb_set_setting('installed_at', time());
|
||||
|
||||
// ④ 口令:只写 bcrypt 哈希,绝不落盘明文
|
||||
// ④ 管理员账号:用户名入库,口令只写 bcrypt 哈希,绝不落盘明文
|
||||
$pwSet = false;
|
||||
if ($setPw && fb_len($pw) >= 8 && $pw === $pw2) {
|
||||
fb_admin_set_username($admUser);
|
||||
if (fb_len($pw) >= 8 && $pw === $pw2) {
|
||||
$pwSet = fb_admin_set_password($pw);
|
||||
}
|
||||
|
||||
@@ -269,28 +275,35 @@ if (isset($_POST['do_install'])) {
|
||||
</section>
|
||||
|
||||
<?php elseif ($done): ?>
|
||||
<?php $pwSet = !empty($GLOBALS['__fb_install_pw_set']); ?>
|
||||
<?php
|
||||
$pwSet = !empty($GLOBALS['__fb_install_pw_set']);
|
||||
$justUser = fb_admin_username();
|
||||
?>
|
||||
<meta http-equiv="refresh" content="2;url=index.php">
|
||||
<section class="narrow">
|
||||
<h1>安装完成 🎉</h1>
|
||||
<p class="muted">已生成 <code>config.php</code>、建好数据库表。下面是接下来要做的两件事:</p>
|
||||
<h1>安装完成</h1>
|
||||
<p class="muted">已建好数据库、写入站点设置。<b>2 秒后自动进入首页</b>(也可点下方按钮)。</p>
|
||||
|
||||
<div class="card-form" style="margin:14px 0">
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
<span><b>① 删除安装文件</b> —— 请把这个 <code>install.php</code> 从服务器上删掉(已安装态下它会拒绝重装,但删掉更干净)。</span>
|
||||
<span><b>② 设置口令</b> ——
|
||||
<?php if ($pwSet): ?>
|
||||
管理员口令已设成你刚才填的那个,并以 <b>bcrypt 哈希</b>存进数据库;<code>config.php</code> 里没有任何明文口令。
|
||||
<?php else: ?>
|
||||
你这次没设口令。<b>在设好之前后台无法登录</b>:请进入 <a href="admin.php">管理后台</a>,在「站点设置 → 登录口令」里设定(或重新运行本向导)。
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<span><b>网站名称</b>:<?php echo e((string)fb_cfg('site_name', '')); ?></span>
|
||||
<span><b>管理员用户名</b>:<?php echo e($justUser !== '' ? $justUser : '(未设置)'); ?></span>
|
||||
<span><b>登录入口</b>:<a href="admin.php">admin.php</a>(用户名 + 口令)</span>
|
||||
<span><b>口令存放</b>:bcrypt 哈希,存于数据库;<code>config.php</code> 里没有任何明文口令。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($err !== ''): ?><?php fb_flash_box($err, 'error'); ?><?php endif; ?>
|
||||
|
||||
<div class="card-form" style="margin:14px 0">
|
||||
<p class="muted small" style="margin:0">
|
||||
<b>建议:装好后把 <code>install.php</code> 从服务器删掉。</b>
|
||||
它在已安装状态下会拒绝重装,但删掉更干净。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a class="btn primary" href="index.php">进入问题列表</a>
|
||||
<a class="btn primary" href="index.php">进入首页</a>
|
||||
<a class="btn" href="admin.php">管理后台</a>
|
||||
</p>
|
||||
</section>
|
||||
@@ -323,57 +336,54 @@ if (isset($_POST['do_install'])) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="margin:22px 0 10px">二、填写站点信息</h2>
|
||||
<h2 style="margin:22px 0 10px">二、初始化站点</h2>
|
||||
<?php fb_flash_box($err, 'error'); ?>
|
||||
<form method="post" action="install.php" class="card-form">
|
||||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||||
<input type="hidden" name="site_desc" value="<?php echo e(isset($_POST['site_desc']) ? (string)$_POST['site_desc'] : ''); ?>">
|
||||
<input type="hidden" name="admin_email" value="<?php echo e(isset($_POST['admin_email']) ? (string)$_POST['admin_email'] : ''); ?>">
|
||||
<input type="hidden" name="base_url" value="<?php echo e(isset($_POST['base_url']) ? (string)$_POST['base_url'] : ''); ?>">
|
||||
<input type="hidden" name="set_password" value="1">
|
||||
|
||||
<label class="field"><span>站点标题 <b class="req">*</b></span>
|
||||
<input type="text" name="site_name" maxlength="80" required
|
||||
value="<?php echo e(isset($_POST['site_name']) ? (string)$_POST['site_name'] : (string)fb_cfg('site_name', '轻反馈')); ?>"
|
||||
placeholder="轻反馈">
|
||||
<i class="hint">显示在浏览器标签、页面顶栏与页脚。用来说明「本系统为哪个软件提供反馈服务」,如「XX 软件 · 问题反馈」。</i></label>
|
||||
<label class="field"><span>网站名称 <b class="req">*</b></span>
|
||||
<input type="text" name="site_name" maxlength="80" required autofocus
|
||||
value="<?php echo e(isset($_POST['site_name']) ? (string)$_POST['site_name'] : (string)fb_cfg('site_name', '')); ?>"
|
||||
placeholder="例如:我的软件 · 问题反馈">
|
||||
<i class="hint">显示在浏览器标签、页面顶栏和页脚。</i></label>
|
||||
|
||||
<label class="field"><span>站点描述</span>
|
||||
<textarea name="site_desc" rows="3" maxlength="300"
|
||||
placeholder="欢迎在这里反馈使用问题、提出功能建议。"><?php echo e(isset($_POST['site_desc']) ? (string)$_POST['site_desc'] : (string)fb_cfg('site_desc', '')); ?></textarea>
|
||||
<i class="hint">显示在首页标题下方,一句话说明这里可以反馈什么。</i></label>
|
||||
|
||||
<div class="grid2">
|
||||
<label class="field"><span>官方邮箱</span>
|
||||
<input type="email" name="admin_email" value="<?php echo e(isset($_POST['admin_email']) ? (string)$_POST['admin_email'] : (string)fb_cfg('admin_email', '')); ?>"
|
||||
placeholder="support@example.com">
|
||||
<i class="hint">对外展示用的联系邮箱,可留空。</i></label>
|
||||
<label class="field"><span>站点根地址</span>
|
||||
<input type="text" name="base_url" value="<?php echo e(isset($_POST['base_url']) ? (string)$_POST['base_url'] : (string)fb_cfg('base_url', '')); ?>"
|
||||
placeholder="https://你的域名/feedback">
|
||||
<i class="hint">用于邮件正文里的链接。留空则按访问域名自动探测。</i></label>
|
||||
</div>
|
||||
|
||||
<h2 style="margin:22px 0 10px">三、设置管理员口令</h2>
|
||||
<p class="muted small" style="margin:-4px 0 10px">
|
||||
口令用来登录管理后台、以及以「官方」身份回复。<b>它以 bcrypt 单向哈希写入数据库,明文不会保存在 config.php 或任何文件里。</b>
|
||||
留空则暂不设定,装完后可在后台补设(补设之前后台不可登录)。
|
||||
<h2 style="margin:20px 0 10px">管理员账号</h2>
|
||||
<p class="muted small" style="margin:-4px 0 12px">
|
||||
用来登录管理后台、以「官方」身份回复。<b>口令以 bcrypt 单向哈希存进数据库,
|
||||
明文不会写进 config.php 或任何文件。</b>
|
||||
</p>
|
||||
|
||||
<label class="field"><span>用户名 <b class="req">*</b></span>
|
||||
<input type="text" name="admin_user" maxlength="32" required
|
||||
value="<?php echo e(isset($_POST['admin_user']) ? (string)$_POST['admin_user'] : 'admin'); ?>"
|
||||
placeholder="admin" autocomplete="username">
|
||||
<i class="hint">字母、数字、下划线、点、连字符,2~32 位。</i></label>
|
||||
|
||||
<div class="grid2">
|
||||
<label class="field"><span>管理员口令(至少 8 位)</span>
|
||||
<input type="password" name="admin_password" autocomplete="new-password"></label>
|
||||
<label class="field"><span>再输入一次</span>
|
||||
<input type="password" name="admin_password2" autocomplete="new-password"></label>
|
||||
<label class="field"><span>口令 <b class="req">*</b></span>
|
||||
<input type="password" name="admin_password" minlength="8" required autocomplete="new-password"></label>
|
||||
<label class="field"><span>再输入一次 <b class="req">*</b></span>
|
||||
<input type="password" name="admin_password2" minlength="8" required autocomplete="new-password"></label>
|
||||
</div>
|
||||
<label class="check"><input type="checkbox" name="set_password" value="1" checked> 现在设定口令(推荐)</label>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn primary" type="submit" name="do_install" value="1"<?php echo $envOk ? '' : ' disabled'; ?>>开始安装</button>
|
||||
<button class="btn primary" type="submit" name="do_install" value="1"<?php echo $envOk ? '' : ' disabled'; ?>>完成安装,进入首页</button>
|
||||
</div>
|
||||
<?php if (!$envOk): ?>
|
||||
<p class="muted small">环境体检未通过前无法提交,请先处理标红项。</p>
|
||||
<p class="muted small">环境体检未通过前无法提交,请先处理上方标红项。</p>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="card-form" style="margin-top:18px">
|
||||
<p class="muted small">安装后再删掉本文件即可。<br>
|
||||
邮件通知(SMTP)与附件上传(阿里云 OSS)都在安装完成后、在后台「站点设置」里配置,这里不需要填。</p>
|
||||
<p class="muted small">
|
||||
其余设置都已自动处理:站点地址按访问域名自动探测,密钥与令牌已自动生成。<br>
|
||||
邮件通知(SMTP)与附件上传(OSS)在装好后到后台「站点设置」里配,这里不用填。<br>
|
||||
<b>装完请删掉 install.php</b>(它本身也会拒绝重复安装)。
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
@@ -300,6 +300,7 @@ function fb_setting_keys() {
|
||||
'oss.verify', 'oss.image_process', 'oss.use_internal_endpoint',
|
||||
'moderation.enabled', 'moderation.close_mail_subject',
|
||||
'admin.password_hash',
|
||||
'admin.username',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -452,6 +453,51 @@ function fb_admin_set_password($pw) {
|
||||
return fb_set_setting('admin.password_hash', password_hash((string)$pw, PASSWORD_DEFAULT));
|
||||
}
|
||||
|
||||
/* ---------------- 管理员账号(用户名 + 口令) ----------------
|
||||
* 用户名存数据库(admin.username),口令存 bcrypt 哈希(admin.password_hash)。
|
||||
* 两者缺一不可:用户名不存在时也照常走一次 password_verify,避免用响应快慢探测账号。
|
||||
*/
|
||||
|
||||
/** 管理员用户名;没设过返回空串 */
|
||||
function fb_admin_username() {
|
||||
return trim((string)fb_setting('admin.username', ''));
|
||||
}
|
||||
|
||||
/** 设置管理员用户名 */
|
||||
function fb_admin_set_username($name) {
|
||||
$name = trim((string)$name);
|
||||
if ($name === '') { return false; }
|
||||
return fb_set_setting('admin.username', $name);
|
||||
}
|
||||
|
||||
/** 账号是否已配置(用户名 + 口令都在) */
|
||||
function fb_admin_account_ready() {
|
||||
return fb_admin_username() !== '' && fb_admin_password_is_set();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验账号口令。
|
||||
* @return int 0=通过 1=账号或口令不对 2=还没配置账号
|
||||
*/
|
||||
function fb_admin_verify_login($user, $pw) {
|
||||
$user = trim((string)$user);
|
||||
$pw = (string)$pw;
|
||||
$u = fb_admin_username();
|
||||
|
||||
if ($u === '' && !fb_admin_password_is_set()) { return 2; }
|
||||
|
||||
// 用户名没配过(老部署只设了口令):只比口令,向后兼容
|
||||
if ($u === '') {
|
||||
return fb_admin_verify_password($pw) ? 0 : 1;
|
||||
}
|
||||
|
||||
$hash = (string)fb_setting('admin.password_hash', '');
|
||||
// 用户名不对也照常跑一次哈希校验,保持耗时一致
|
||||
$pwOk = $hash !== '' ? password_verify($pw, $hash) : fb_admin_verify_password($pw);
|
||||
$uOk = hash_equals($u, $user);
|
||||
return ($uOk && $pwOk) ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭一个 curl 句柄(跨版本安全)。
|
||||
* --------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user