需求3脱敏:config.php改为占位符分发(敏感信息不入库),清除admin.php公网前端13处与land_api.php的硬编码SECRET_KEY,改为服务端注入/常量比对

This commit is contained in:
ypsdf1
2026-09-30 11:18:41 +08:00
parent a46737d64b
commit b7b2647ef1
3 changed files with 57 additions and 36 deletions
+17 -14
View File
@@ -190,6 +190,11 @@ header('Pragma: no-cache');
</div>
<script data-cfasync="false">
// ★ 通信密钥与 API 地址:定义放到 script 最前,避免下方代码在 const 初始化前引用(TDZ)。
// 真实密钥值只存在 config.php 里(不入库),运行时由服务端注入,源码里没有字面量。
const SECRET = <?php echo json_encode((string)SECRET_KEY, JSON_UNESCAPED_UNICODE); ?>;
const LAND_API = 'api/land_api.php';
// ★ 全局错误处理器:捕获所有未处理异常
window.onerror = function(msg, src, line, col, err) {
console.error('[GlobalError]', msg, 'at', src, 'line', line + ':' + col, err);
@@ -2553,9 +2558,9 @@ async function loadLands(el) {
el.innerHTML = '<div style="text-align:center;padding:40px;color:var(--dim)">加载中...</div>';
try {
const [landsRes, shopRes, configRes] = await Promise.all([
fetch('api/land_api.php?action=list_lands&secret=sdf1_web_comm_2026_ypshidifu').then(r => r.json()),
fetch('api/land_api.php?action=list_shop&secret=sdf1_web_comm_2026_ypshidifu').then(r => r.json()),
fetch('api/land_api.php?action=get_config&secret=sdf1_web_comm_2026_ypshidifu').then(r => r.json())
fetch('api/land_api.php?action=list_lands&secret=' + SECRET + '').then(r => r.json()),
fetch('api/land_api.php?action=list_shop&secret=' + SECRET + '').then(r => r.json()),
fetch('api/land_api.php?action=get_config&secret=' + SECRET + '').then(r => r.json())
]);
let html = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:8px">';
@@ -2678,12 +2683,12 @@ async function saveLandConfig() {
await fetch('api/land_api.php?action=update_config', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'key=create_price_per_sqm&value=' + encodeURIComponent(price) + '&secret=sdf1_web_comm_2026_ypshidifu'
body: 'key=create_price_per_sqm&value=' + encodeURIComponent(price) + '&secret=' + SECRET + ''
});
await fetch('api/land_api.php?action=update_config', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'key=max_lands_per_player&value=' + encodeURIComponent(maxLands) + '&secret=sdf1_web_comm_2026_ypshidifu'
body: 'key=max_lands_per_player&value=' + encodeURIComponent(maxLands) + '&secret=' + SECRET + ''
});
document.querySelector('.modal-close')?.click();
loadLands(document.getElementById('C'));
@@ -2697,7 +2702,7 @@ async function deleteLand(name) {
fetch('api/land_api.php?action=delete_land', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'name=' + encodeURIComponent(name) + '&secret=sdf1_web_comm_2026_ypshidifu'
body: 'name=' + encodeURIComponent(name) + '&secret=' + SECRET + ''
}).then(r => r.json()).then(d => {
if (d.success) loadLands(document.getElementById('C'));
else glassAlert('失败: ' + (d.error||''));
@@ -2724,7 +2729,7 @@ async function adminTransferLand(name, currentOwner) {
const res = await fetch('api/land_api.php?action=update_land_owner', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'name=' + encodeURIComponent(name) + '&owner=' + encodeURIComponent(trimmed) + '&secret=sdf1_web_comm_2026_ypshidifu'
body: 'name=' + encodeURIComponent(name) + '&owner=' + encodeURIComponent(trimmed) + '&secret=' + SECRET + ''
});
const d = await res.json();
if (d.success && d.pending) {
@@ -2735,7 +2740,7 @@ async function adminTransferLand(name, currentOwner) {
const interval = 3000; // 3秒轮询一次
const poll = async () => {
try {
const pollRes = await fetch('api/land_api.php?action=get_owner_change_status&name=' + encodeURIComponent(name) + '&secret=sdf1_web_comm_2026_ypshidifu');
const pollRes = await fetch('api/land_api.php?action=get_owner_change_status&name=' + encodeURIComponent(name) + '&secret=' + SECRET + '');
const pollData = await pollRes.json();
if (pollData.success) {
if (pollData.status === 'completed') {
@@ -2757,7 +2762,7 @@ async function adminTransferLand(name, currentOwner) {
setTimeout(poll, interval);
} else {
// 超时,检查最终状态
const finalRes = await fetch('api/land_api.php?action=get_owner_change_status&name=' + encodeURIComponent(name) + '&secret=sdf1_web_comm_2026_ypshidifu');
const finalRes = await fetch('api/land_api.php?action=get_owner_change_status&name=' + encodeURIComponent(name) + '&secret=' + SECRET + '');
const finalData = await finalRes.json();
if (finalData.success && finalData.status === 'completed') {
glassAlert('改主成功!领地 [' + name + '] 所有者已变更为 [' + trimmed + ']');
@@ -2806,7 +2811,7 @@ async function deleteShopItem(id) {
fetch('api/land_api.php?action=delete_shop_item', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'id=' + id + '&secret=sdf1_web_comm_2026_ypshidifu'
body: 'id=' + id + '&secret=' + SECRET + ''
}).then(r => r.json()).then(d => {
if (d.success) loadLands(document.getElementById('C'));
else glassAlert('失败: ' + (d.error||''));
@@ -2814,8 +2819,6 @@ async function deleteShopItem(id) {
}
// ==================== 用户组管理 ====================
const SECRET = 'sdf1_web_comm_2026_ypshidifu';
const LAND_API = 'api/land_api.php';
// 权限名称映射(中文名→Java key)
const PERM_NAMES = {
@@ -2861,7 +2864,7 @@ async function loadUserGroups(el) {
try {
const [res, configRes] = await Promise.all([
apiCall('list_user_groups'),
fetch('api/land_api.php?action=get_config&secret=sdf1_web_comm_2026_ypshidifu').then(r => r.json()).catch(() => ({config:{}}))
fetch('api/land_api.php?action=get_config&secret=' + SECRET + '').then(r => r.json()).catch(() => ({config:{}}))
]);
const groups = res.groups || [];
const homeCfg = (configRes.config && configRes.config.max_home_per_player) || '5';
@@ -2956,7 +2959,7 @@ async function saveHomeConfig() {
await fetch('api/land_api.php?action=update_config', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'key=max_home_per_player&value=' + encodeURIComponent(maxHome) + '&secret=sdf1_web_comm_2026_ypshidifu'
body: 'key=max_home_per_player&value=' + encodeURIComponent(maxHome) + '&secret=' + SECRET + ''
});
document.querySelector('.modal-close')?.click();
loadUserGroups(document.getElementById('C'));
+8 -5
View File
@@ -6,6 +6,9 @@
*/
header('Content-Type: application/json; charset=utf-8');
error_reporting(E_ERROR | E_PARSE);
// 取 SECRET_KEY(插件与 Web 端约定的通信密钥,值在 config.php 里由部署者自己填)。
// require_once:本文件也可能被别的入口 include,不会重复定义。
require_once __DIR__ . '/../config.php';
/**
* ★ 将Java自定义格式转换为JSON数组
@@ -3423,9 +3426,9 @@ function handleTransferStatus($db, $playerName, $landName) {
}
function validateSecret($secret) {
if (empty($secret)) return false;
$validSecrets = [
'sdf1_web_comm_2026_ypshidifu'
];
return in_array($secret, $validSecrets);
// 密钥值只存在 config.php 里(部署者自己填、不入库),这里一律比对 SECRET_KEY,
// 不再在代码里硬编码任何真实密钥。
if (!is_string($secret) || $secret === '') return false;
if (!defined('SECRET_KEY') || SECRET_KEY === '') return false;
return hash_equals((string)SECRET_KEY, $secret);
}
+32 -17
View File
@@ -1,10 +1,22 @@
<?php
/**
* Web通信系统 - 配置文件
*
* ★ 本文件随仓库分发,里面的值全部是占位符 —— 请改成你自己的。
* 任何真实口令 / 密钥 / IP / 令牌都严禁写进仓库(2026-09-29 用户指令)。
* 部署脚本也不会上传本文件,线上 config.php 只在服务器上由 admin_2fa_setup.php 改写。
*
* 拿到代码第一次要改的东西:
* 1. SECRET_KEY —— 插件(Java)与本 Web 端必须一字不差地相同,否则所有同步接口 401
* 2. ADMIN_PASS —— 后台管理密码
* 3. SMTP_* —— 邮件发信用途(不发信可以不管)
* 4. GAME_*_DB / DIR —— 你服务器上游戏数据库的实际路径
* 最下面 SEC-UPDATE 区故意是空的:后台访问令牌、IP 白名单、二次验证这三项
* 在你自己的服务器上首次生成 / 由你自己填,不会进仓库。
*/
// ===== 安全密钥(插件和Web端必须一致) =====
define('SECRET_KEY', 'sdf1_web_comm_2026_ypshidifu');
define('SECRET_KEY', 'REPLACE_ME_SECRET_KEY');
// ===== Token配置 =====
define('TOKEN_EXPIRE_SECONDS', 86400); // 24小时
@@ -17,12 +29,12 @@ define('ORDERS_DB_PATH', __DIR__ . '/db/orders.db');
// ===== 管理员认证 =====
define('ADMIN_USER', 'admin');
define('ADMIN_PASS', 'ypshidifu2026'); // 管理员密码
define('ADMIN_PASS', 'REPLACE_ME_ADMIN_PASSWORD'); // 管理员密码
// ===== 游戏数据库路径(用于同步) =====
define('GAME_BOND_DB', 'D:/服务器/插件/bond.db');
define('GAME_LOGIN_DB', 'D:/服务器/插件/login.db');
define('GAME_SHOP_DIR', 'D:/服务器/插件/shop/');
define('GAME_BOND_DB', 'REPLACE_ME_GAME_BOND_DB_PATH');
define('GAME_LOGIN_DB', 'REPLACE_ME_GAME_LOGIN_DB_PATH');
define('GAME_SHOP_DIR', 'REPLACE_ME_GAME_SHOP_DIR_PATH');
// ===== Web子目录路径(如 /plugin 或 /test1,根目录则留空) =====
define('WEBSUB_DIR', '/plugin');
@@ -39,28 +51,31 @@ function webPath($path = '') {
}
// ===== SMTP邮件配置 =====
define('SMTP_HOST', 'hwsmtp.exmail.qq.com');
define('SMTP_HOST', 'REPLACE_ME_SMTP_HOST');
define('SMTP_PORT', '465');
define('SMTP_USER', 'mcserver@ypshidifu.cn');
define('SMTP_PASS', 'sQ2ZiCZGq96xi9Sv');
define('SMTP_USER', 'REPLACE_ME_SMTP_USER');
define('SMTP_PASS', 'REPLACE_ME_SMTP_PASSWORD');
define('SMTP_SENDER_NAME', 'Sdf1_login');
define('SMTP_USE_SSL', true);
// ===== 紧急安全加固(2026-09-29 入侵事件响应,实现见 security.php)=====
// 第一层:TOTP 二次验证 —— 1Password / Microsoft Authenticator 等标准 6 位动态码(仅校验,无推送)。
// 第二层:管理面 IP 白名单 —— ADMIN_IP_WHITELIST 为空数组时,仅服务器本机 127.0.0.1
// 与 boot 引导令牌可访问后台,其余任何人一律 403。
// 第二层:管理面 IP 白名单 —— 未配置白名单时,仅服务器本机 127.0.0.1 与持有 boot 引导令牌的
// 人可访问后台,其余任何人一律 403。
// 第三层:admin.php 动态访问令牌 —— 只有 https://域名/plugin/admin.php?token=<SEC_ACCESS_TOKEN>
// 才能打开后台;不带令牌(或令牌错)的请求一律返回 nginx 原生 404 页,
// 与"服务器上根本没有这个文件"表现完全一致,探测者无从判断。
// 被锁在外面时的恢复方法:宝塔/SSH 查看本文件的 SEC_BOOT_TOKEN,访问
//
// 下面这个区段故意不写任何 define —— 这些是每个部署者自己的安全隐私,严禁入库:
// SEC_BOOT_TOKEN 引导令牌(被锁在外面时的逃生舱)
// SEC_ACCESS_TOKEN 后台访问令牌(第三层门禁)
// SEC_EPOCH 安全纪元(部署加固时作废此前的全部活会话)
// ADMIN_IP_WHITELIST 你自己的 IP 白名单
// SEC_2FA_ENABLED / SEC_2FA_SECRET 你自己的二次验证开关与密钥
// 首次访问 https://域名/plugin/admin_2fa_setup.php 并保存任意一项时,缺失的键会由该页
// 在你自己的服务器上随机生成并就地补全(写前自动备份到 db/config_bak/)。
// 被锁在外面时的恢复方法:宝塔/SSH 查看本文件补全后的 SEC_BOOT_TOKEN,访问
// https://你的域名/plugin/admin_2fa_setup.php?boot=<令牌>
// 进入配置页把自己当前 IP 加入白名单(boot 令牌本身也是配置页入口)。
// >>>SEC-UPDATE-BEGIN 本区段由 admin_2fa_setup.php 自动维护,请勿手工改动标记行
define('SEC_BOOT_TOKEN', '2132ac30a611aa6df3a8a78d17396385');
define('SEC_ACCESS_TOKEN', '7c40ad41e5a384ef4f496088a955f8f0');
define('SEC_EPOCH', 1790687661);
define('ADMIN_IP_WHITELIST', array('119.39.100.21'));
define('SEC_2FA_ENABLED', false);
define('SEC_2FA_SECRET', '');
// >>>SEC-UPDATE-END