第三层(新增,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 往返解码校验。
100 lines
4.4 KiB
PHP
100 lines
4.4 KiB
PHP
<?php
|
|
// ★ 紧急加固(2026-09-29):第二层 IP 白名单 —— 登录页本身也只对白名单 IP 开放
|
|
require_once __DIR__ . '/security.php';
|
|
secGatePage();
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SDF1 - 管理后台登录</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
background: #0d1117; color: #e6edf3;
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
display: flex; justify-content: center; align-items: center;
|
|
min-height: 100vh;
|
|
}
|
|
.login-box {
|
|
background: #161b22; border: 1px solid #30363d; border-radius: 12px;
|
|
padding: 40px; width: 380px; max-width: 90%;
|
|
}
|
|
.login-box h1 { text-align: center; color: #58a6ff; margin-bottom: 8px; font-size: 24px; }
|
|
.login-box .sub { text-align: center; color: #8b949e; font-size: 13px; margin-bottom: 28px; }
|
|
.field { margin-bottom: 16px; }
|
|
.field label { display: block; font-size: 13px; color: #8b949e; margin-bottom: 6px; }
|
|
.field input {
|
|
width: 100%; padding: 10px 14px; background: #0d1117; border: 1px solid #30363d;
|
|
border-radius: 6px; color: #e6edf3; font-size: 14px; outline: none; transition: border-color 0.2s;
|
|
}
|
|
.field input:focus { border-color: #58a6ff; }
|
|
.login-btn {
|
|
width: 100%; padding: 12px; background: #58a6ff; color: #fff; border: none;
|
|
border-radius: 6px; font-size: 15px; font-weight: 600; cursor: pointer;
|
|
transition: background 0.2s; margin-top: 8px;
|
|
}
|
|
.login-btn:hover { background: #79c0ff; }
|
|
.login-btn:disabled { background: #30363d; cursor: not-allowed; }
|
|
.error { color: #f85149; font-size: 13px; text-align: center; margin-top: 12px; display: none; }
|
|
.links { text-align: center; margin-top: 20px; }
|
|
.links a { color: #58a6ff; text-decoration: none; font-size: 13px; }
|
|
.links a:hover { text-decoration: underline; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="login-box">
|
|
<h1>🔐 管理后台</h1>
|
|
<div class="sub">SDF1 插件管理系统</div>
|
|
<div class="field">
|
|
<label>管理密码</label>
|
|
<input type="password" id="password" placeholder="请输入管理密码" autofocus>
|
|
</div>
|
|
<button class="login-btn" id="loginBtn" onclick="doLogin()">登 录</button>
|
|
<div class="error" id="errorMsg"></div>
|
|
<div class="links"><a href="player.php">← 返回玩家商城</a></div>
|
|
</div>
|
|
|
|
<script>
|
|
document.getElementById('password').addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') doLogin();
|
|
});
|
|
|
|
async function doLogin() {
|
|
const pw = document.getElementById('password').value;
|
|
if (!pw) return;
|
|
document.getElementById('loginBtn').disabled = true;
|
|
document.getElementById('loginBtn').textContent = '登录中...';
|
|
|
|
try {
|
|
const res = await fetch('api/admin.php?action=login', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({password: pw})
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
// ★ 第三层:登录接口在密码正确后才下发入口令牌,用来拼 admin.php?token=
|
|
const tk = (data.data && data.data.token) ? ('?token=' + encodeURIComponent(data.data.token)) : '';
|
|
// ★ 第一层:启用二次验证时先进 6 位动态码验证页,通过后再进后台
|
|
if (data.data && data.data.need_2fa) {
|
|
window.location.href = 'admin_2fa.php';
|
|
} else {
|
|
window.location.href = 'admin.php' + tk;
|
|
}
|
|
} else {
|
|
document.getElementById('errorMsg').textContent = data.message;
|
|
document.getElementById('errorMsg').style.display = 'block';
|
|
}
|
|
} catch (e) {
|
|
document.getElementById('errorMsg').textContent = '网络错误';
|
|
document.getElementById('errorMsg').style.display = 'block';
|
|
}
|
|
document.getElementById('loginBtn').disabled = false;
|
|
document.getElementById('loginBtn').textContent = '登 录';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|