紧急加固三层:admin.php 入口令牌门禁 + 2FA 扫码绑定与精确诊断

第三层(新增,config.php 的 SEC_ACCESS_TOKEN):
- 只有 admin.php?token=<令牌> 才放行,缺/错令牌一律返回 nginx 原生 404
  (与"文件真的不存在"逐字节一致,探测者无从判断),令牌检查置于白名单之前
- 登录接口在密码校验通过后才下发 token,2FA 通过与收银台跳转由服务端补拼
- 入口页加 Referrer-Policy: no-referrer,防令牌经外链 Referer 泄露
- 配置页展示后台唯一入口地址,支持一键重新生成令牌

第一层修复(2FA 验证码"无效"):
- secTotpVerify 容错窗口 ±30 秒扩到 ±90 秒
- 新增 secTotpDiagnose:失败时区分"密钥没对上"与"设备时钟偏移多少秒"并给出处置指引
- Base32 解码兼容空格/连字符与 0/1/8/9 手误,避免密钥被静默截短
- 验证页倒计时与服务器时间显示改用服务端时钟基准
- 密钥生成改幂等,"重新生成"二次确认,防止已扫码密钥被误作废

第二层(二维码):
- admin_2fa_setup.php 内联 qrcode-generator(MIT) 生成 otpauth 二维码,离线可用
- 后台侧边栏新增"安全配置"入口;admin_2fa_setup.php?boot= 在白名单 IP 下同样生效

已通过线上端到端验证 22 项 + 二维码 OpenCV 往返解码校验。
This commit is contained in:
ypsdf1
2026-09-29 22:55:46 +08:00
parent fd03189f31
commit c46c9ebdfe
9 changed files with 3622 additions and 9 deletions
+36 -2
View File
@@ -41,6 +41,10 @@ if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// ★ 紧急加固(2026-09-29):第二层 IP 白名单 —— 白名单未配置时,
// 任何外部 IP 连 login/status 也一律 403(127.0.0.1 本机永放行)
secGateApi();
$action = getParam('action', 'status');
try {
@@ -89,10 +93,33 @@ switch ($action) {
// ===== 各函数定义 =====
function adminDoLogin() {
// ★ 紧急加固:同 IP 暴力破解限速(5 次失败锁定 10 分钟),失败写审计日志
$ip = function_exists('secClientIp') ? secClientIp() : '';
if (function_exists('secThrottleLocked')) {
$lock = secThrottleLocked('login_fail', $ip, 5, 600);
if ($lock > 0) {
if (function_exists('secLog')) secLog('login_throttled', 'lock=' . $lock . 's');
exit(json_encode(['success' => false, 'message' => '尝试次数过多,请 ' . $lock . ' 秒后再试', 'code' => 429], JSON_UNESCAPED_UNICODE));
}
}
$password = getParam('password');
if (!$password) exit(json_encode(['success' => false, 'message' => 'Missing password'], JSON_UNESCAPED_UNICODE));
if (adminLogin($password)) {
exit(json_encode(['success' => true, 'data' => ['login_time' => time()], 'message' => 'OK'], JSON_UNESCAPED_UNICODE));
if (function_exists('secThrottleReset')) secThrottleReset('login_fail', $ip);
if (function_exists('secLog')) secLog('login_pass', 'password_ok');
// ★ 第一层:已启用二次验证时,告知前端跳转 6 位动态码验证页
$need2fa = function_exists('secSessionNeed2FA') && secSessionNeed2FA();
// ★ 第三层:登录成功后把后台入口令牌交给前端,用于拼 admin.php?token=...
// —— 令牌只在密码正确之后才出现,登录页 HTML 里不含它。
$tok = function_exists('secToken') ? secToken() : '';
exit(json_encode(['success' => true, 'data' => ['login_time' => time(), 'need_2fa' => $need2fa, 'token' => $tok], 'message' => 'OK'], JSON_UNESCAPED_UNICODE));
}
if (function_exists('secThrottleFail')) {
$left = secThrottleFail('login_fail', $ip, 5, 600);
if (function_exists('secLog')) secLog('login_fail', 'bad_password' . ($left > 0 ? ' LOCKED_' . $left . 's' : ''));
if ($left > 0) {
exit(json_encode(['success' => false, 'message' => '尝试次数过多,请 ' . $left . ' 秒后再试', 'code' => 429], JSON_UNESCAPED_UNICODE));
}
}
exit(json_encode(['success' => false, 'message' => '密码错误', 'code' => 401], JSON_UNESCAPED_UNICODE));
}
@@ -103,9 +130,16 @@ function adminDoLogout() {
}
function adminStatus() {
$in = function_exists('isAdminLoggedIn') ? isAdminLoggedIn() : false;
// ★ 第一层:前端据 need_2fa 决定跳验证页还是直进后台
$need2fa = $in && function_exists('secSessionNeed2FA') && secSessionNeed2FA();
exit(json_encode([
'success' => true,
'data' => ['logged_in' => isAdminLoggedIn(), 'login_time' => $_SESSION['admin_login_time'] ?? 0],
'data' => [
'logged_in' => $in,
'login_time' => $_SESSION['admin_login_time'] ?? 0,
'need_2fa' => $need2fa
],
'message' => 'ok'
], JSON_UNESCAPED_UNICODE));
}