页脚入口与安装向导;加固管理员口令存储

需求落实:
1. 页脚常驻「管理后台」链接,不用再手敲 admin.php。
2. 页脚「由 FeedLite 驱动」中的 FeedLite 改为超链接(指向开源仓库);
   后台新增「页脚」设置组,可关闭整句(关闭后只留管理后台链接),也可改链接地址。
3. 管理员口令存储方式在后台如实展示:数据库 settings.admin.password_hash,
   存 bcrypt 单向哈希(不可逆),明文不落盘。
4. 新增安装向导 install.php:data/ 下没有数据库时自动 302 引导;
   体检环境 -> 填站点信息与口令 -> 生成 config.php -> 建库建表 -> 完成。

顺带修掉一个真实安全隐患:
  config.php 里的出厂占位口令 CHANGE_ME_admin 原先可被当作有效口令登录后台
  (等于公开仓库里就写着线上后门口令)。现改为占位/空值一律拒绝,
  未设口令时后台明确提示去安装向导,不再含糊报「口令不正确」。

实现要点:
- 新增 fb_cfg_file():只读配置文件、不叠加 settings 表、绝不触达数据库。
  「是否已安装」以数据库文件是否存在为判据,而 fb_cfg() 读 settings 会触发
  fb_db() 就地建库 —— 那会把安装态自己造出来,所以判定必须走不碰库的通道。
- bootstrap 只在已安装时建库;未安装则 302 到 install.php(CLI 下报错退出)。
- fb_settings_flat() 在未安装时直接返回空,杜绝任何隐式建库。
- install.php 用文本级替换生成 config.php,保留全部注释与 OSS/SMTP 段落;
  生成的 PHP 用 token_get_all(TOKEN_PARSE) 校验后才落盘。
- 页脚链接只放行 http/https,后台保存时校验(防 javascript: 伪协议)。
- style.css 页脚行改 flex,版本号 v6 -> v7。

验证:
- php -l 全量通过;现有回归 25/25、附件专项 13/13 保持通过。
- 安装向导专项 60/60(含「明文口令未写入任何文件」「数据库字节里搜不到明文」)。
- 反向验证:撤销三处修复后对应断言确实失败(断言非装饰)。
- 几何检查:本机 15/15 + 页脚专项全过;线上真实 URL 15/15 + 页脚专项全过。
- 线上真机冒烟 22/22。
This commit is contained in:
2026-09-28 22:53:55 +08:00
parent 1401ad899b
commit 27187b38f1
8 changed files with 654 additions and 39 deletions
+75 -12
View File
@@ -30,6 +30,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
$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');
@@ -46,7 +48,10 @@ if (!fb_admin_logged_in()) {
<div class="crumbs"><a href="index.php">← 返回问题列表</a></div>
<section class="narrow">
<h1>管理登录</h1>
<p class="muted">请输入管理员口令(初始口令在 <code>config.php</code> 的 <code>admin_password</code>,登录后在「站点设置」里可以改掉它)。</p>
<p class="muted">请输入管理员口令<?php if (fb_admin_password_source() === 'file'): ?>(当前仍是 <code>config.php</code> 里的 <code>admin_password</code> 明文,登录后请到「站点设置」改成加密存储)<?php endif; ?>。</p>
<?php if (fb_admin_password_source() === 'none'): ?>
<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()); ?>">
@@ -184,7 +189,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
$old = (string)(isset($_POST['old_password']) ? $_POST['old_password'] : '');
$n1 = (string)(isset($_POST['new_password']) ? $_POST['new_password'] : '');
$n2 = (string)(isset($_POST['new_password2']) ? $_POST['new_password2'] : '');
if (!fb_admin_verify_password($old)) {
// 从没设过口令(或仍是出厂占位值)时,跳过"当前口令"校验 —— 否则谁也进不来、又改不了
$firstSet = !fb_admin_password_is_set();
if (!$firstSet && !fb_admin_verify_password($old)) {
$msg = '当前口令不正确。';
$msgType = 'error';
} elseif (fb_len($n1) < 8) {
@@ -195,8 +202,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
$msgType = 'error';
} else {
fb_admin_set_password($n1);
fb_mod_log('password');
$msg = '口令已修改,下次登录请使用新口令。';
fb_mod_log('password', null, $firstSet ? '首次设定登录口令(bcrypt 哈希存库)' : '修改登录口令');
$msg = $firstSet ? '口令已设定,下次登录请使用它。' : '口令已修改,下次登录请使用新口令。';
}
}
}
@@ -223,6 +230,16 @@ function fb_admin_save_settings() {
else { fb_set_setting($k, $v); }
}
// 页脚:是否显示「由 FeedLite 驱动」这句版权,以及链接指向的开源仓库地址
fb_set_setting('footer.show_credit', $post('footer_show_credit') ? 1 : 0);
$curl = trim((string)$post('footer_credit_url', ''));
// 只接受 http/https:这条会被直接渲染成 <a href>,不能让 javascript:/data: 混进来
if ($curl !== '' && !preg_match('~^https?://~i', $curl)) {
return '页脚链接地址需以 http:// 或 https:// 开头。';
}
if ($curl === '' || $curl === fb_credit_url_default()) { fb_unset_setting('footer.credit_url'); }
else { fb_set_setting('footer.credit_url', $curl); }
$ints = [
'upload.image_max_per_post' => [0, 20],
'upload.video_max_per_post' => [0, 10],
@@ -544,6 +561,14 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
placeholder="欢迎在这里反馈使用问题、提出功能建议。"><?php echo e((string)fb_cfg('site_desc', '')); ?></textarea>
<i class="hint">显示在首页标题下方,一句话说明这里可以反馈什么。两项都留空则恢复为出厂默认。</i></label>
<h2 style="margin:24px 0 10px">页脚</h2>
<label class="check"><input type="checkbox" name="footer_show_credit" value="1"<?php echo fb_cfg('footer.show_credit', true) ? ' checked' : ''; ?>> 在页脚显示「由 FeedLite 驱动」
<span class="hint" style="display:block;margin:2px 0 0 24px">取消勾选后,这句话连同链接都不再出现在页脚,只保留「管理后台」入口。</span></label>
<label class="field"><span>FeedLite 链接地址</span>
<input type="text" name="footer_credit_url" maxlength="300"
value="<?php echo e((string)fb_cfg('footer.credit_url', '') !== '' ? (string)fb_cfg('footer.credit_url') : fb_credit_url_default()); ?>">
<i class="hint">页脚「FeedLite」二字指向的地址,默认是本项目开源仓库。只接受 http:// 或 https:// 开头的网址;填回默认值即恢复。</i></label>
<h2 style="margin:24px 0 10px">附件存储</h2>
<label class="field">
<span>附件保存在哪</span>
@@ -649,19 +674,57 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
</div>
</div>
<h2 style="margin:26px 0 10px">修改登录口令</h2>
<h2 style="margin:26px 0 10px">登录口令</h2>
<div class="card-form" style="margin:0 0 16px">
<?php $pwSrc = fb_admin_password_source(); ?>
<div style="display:flex;flex-direction:column;gap:6px">
<span>存储位置:
<?php if ($pwSrc === 'db'): ?>
数据库 <code>settings</code> 表(键名 <code>admin.password_hash</code>)
<?php elseif ($pwSrc === 'file'): ?>
<code>config.php</code> 的 <code>admin_password</code>
<?php else: ?>
<b style="color:#b45309">尚未设置</b>
<?php endif; ?>
</span>
<span>是否加密:
<?php if ($pwSrc === 'db'): ?>
<b>是</b> —— 存的是 <code>password_hash()</code> 生成的 bcrypt 单向哈希(<code>$2y$…</code>),
不可逆、无法还原明文;校验用 <code>password_verify()</code>。
<?php elseif ($pwSrc === 'file'): ?>
<b>否</b> —— 目前是明文写在 <code>config.php</code> 里。
<a href="#pwform">在下面改一次口令</a>后即会转存为数据库中的 bcrypt 哈希。
<?php else: ?>
—— 还没有可用口令,当前后台<strong>无法登录</strong>。请用下方表单设定一个。
<?php endif; ?>
</span>
<?php if ($pwSrc === 'db'): ?>
<span class="muted small">数据库文件在 <code>data/feedback.db</code>(已由 .htaccess 拒绝外部访问),且只存哈希,明文不落盘。</span>
<?php endif; ?>
</div>
</div>
<div id="pwform">
<form method="post" action="admin.php?tab=settings" class="card-form">
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
<div class="grid2">
<label class="field"><span>当前口令</span><input type="password" name="old_password" autocomplete="current-password"></label>
<label class="field"><span>新口令(至少 8 位)</span><input type="password" name="new_password" autocomplete="new-password"></label>
<label class="field"><span>再输入一次新口令</span><input type="password" name="new_password2" autocomplete="new-password"></label>
</div>
<?php if ($pwSrc !== 'none'): ?>
<div class="grid2">
<label class="field"><span>当前口令</span><input type="password" name="old_password" autocomplete="current-password"></label>
<label class="field"><span>新口令(至少 8 位)</span><input type="password" name="new_password" autocomplete="new-password"></label>
<label class="field"><span>再输入一次新口令</span><input type="password" name="new_password2" autocomplete="new-password"></label>
</div>
<?php else: ?>
<div class="grid2">
<label class="field"><span>设定登录口令(至少 8 位)</span><input type="password" name="new_password" autocomplete="new-password"></label>
<label class="field"><span>再输入一次</span><input type="password" name="new_password2" autocomplete="new-password"></label>
</div>
<?php endif; ?>
<div class="form-actions">
<button class="btn" type="submit" name="act" value="password">修改口令</button>
<button class="btn primary" type="submit" name="act" value="password"><?php echo $pwSrc === 'none' ? '设定口令' : '修改口令'; ?></button>
</div>
<p class="muted small">新口令保存在数据库里,优先级高于 <code>config.php</code> 的 <code>admin_password</code>;改完立即生效。</p>
<p class="muted small">新口令以 bcrypt 哈希写入数据库,优先级高于 <code>config.php</code> 的 <code>admin_password</code>;改完立即生效,明文不会留在任何文件里。</p>
</form>
</div>
<?php else: ?>
<?php