用户端 player.php 接入人机验证挂件:三个登录入口统一挂载并提交前取 token

This commit is contained in:
ypsdf1
2026-10-01 15:39:10 +08:00
parent 1a221330dc
commit 8fa7eb5c95
+74 -3
View File
@@ -1,5 +1,6 @@
<?php
require_once __DIR__ . '/security.php'; // 第四层:全局限流(2026-09-30)
require_once __DIR__ . '/captcha_guard.php'; // ★ 人机验证统一接入(2026-10-02):CF/VA 由 config.php 的 CAPTCHA_PROVIDER 单点决定
// ★ 强制缓存失效:用文件修改时间作为版本号
// 每次修改player.php后,文件时间戳变化 → URL不同 → 浏览器必须获取新内容
header('Cache-Control: no-cache, no-store, must-revalidate, max-age=0');
@@ -318,6 +319,7 @@ if ($currentVersion !== $BUILD_VERSION) {
<div class="status-msg" id="glassLoginStatus"></div>
<input type="text" id="glassPlayerName" placeholder="玩家名" maxlength="16" autocomplete="username">
<input type="password" id="glassPassword" placeholder="游戏内密码" maxlength="32" autocomplete="current-password">
<div id="cgSlotGlass"></div>
<button class="login-btn" id="glassLoginBtn" onclick="doGlassLogin()">登 录</button>
<p class="hint">也可在游戏中输入 <code>/sdf1_login weblogin</code> 获取链接</p>
</div>
@@ -335,6 +337,44 @@ if ($currentVersion !== $BUILD_VERSION) {
</div>
<script>
// ★ 人机验证挂件HTML:由 config.php 的 CAPTCHA_PROVIDER 单点决定 CF/VA(全项目只用一家,不混用)。
// 未启用(off / 密钥未配)时为空串,前端不挂件、服务端同步放行,
// 不会出现"服务端要验证、前端却没组件"的错配。
// JSON_HEX_TAG 必须带:HTML 里含 </script>,不转义会把本 <script> 提前截断。
window.__CG_HTML = <?php echo json_encode(cgWidgetHtml(), JSON_HEX_TAG | JSON_UNESCAPED_UNICODE); ?>;
// ===== 人机验证挂件:全页唯一实例,挂到当前可见的登录表单 =====
// 三个入口(主登录页 / 毛玻璃弹窗 / 安全验证弹窗)共用一个 cgBox,避免重复 id;
// innerHTML 插入的 <script> 不会被浏览器执行 → 这里手动执行一次。
function cgMount(slotId) {
var slot = document.getElementById(slotId);
if (!slot) return false;
var html = window.__CG_HTML || '';
if (!html) return false; // 验证码未启用
if (slot.querySelector('#cgBox')) return true; // 本表单已挂过 → 保留已通过的 token
var olds = document.querySelectorAll('.cg-box'); // 其它表单里的旧实例 → 移除(全页唯一)
for (var i = 0; i < olds.length; i++) {
var p = olds[i].parentNode;
if (p) p.removeChild(olds[i]);
}
slot.innerHTML = html;
var sc = slot.querySelector('script');
if (sc) {
var s = document.createElement('script');
s.textContent = sc.textContent;
document.head.appendChild(s);
if (sc.parentNode) sc.parentNode.removeChild(sc);
}
return !!window.CaptchaGuard;
}
// 提交前取 token:未启用时返回 {}(服务端同步放行),否则等待用户完成验证
async function cgPayload() {
if (!(window.__CG_HTML || '')) return {};
if (!window.CaptchaGuard) throw new Error('验证组件未加载,请刷新页面重试');
return await window.CaptchaGuard.payload();
}
// ★ 防止bfcache(前进/后退缓存)绕过安全检查
window.addEventListener('pageshow', function(e) {
if (e.persisted) {
@@ -609,6 +649,7 @@ if ($currentVersion !== $BUILD_VERSION) {
<div id="authPasswordTab">
<div class="row"><label>游戏登录密码</label><input type="password" id="authPassword" placeholder="输入游戏内密码" autofocus></div>
<div style="text-align:right;margin-top:4px"><a href="javascript:void(0)" onclick="showResetPasswordModal()" style="color:var(--accent);font-size:12px">忘记密码?</a></div>
<div id="cgSlotAuth"></div>
</div>
<div id="authEmailTab" style="display:none">
<div class="row"><label>绑定邮箱</label>
@@ -620,6 +661,7 @@ if ($currentVersion !== $BUILD_VERSION) {
</div>
</div>
<div id="authError" style="color:var(--red);font-size:12px;margin-top:4px"></div>`;
cgMount('cgSlotAuth'); // ★ 挂载人机验证(唯一实例)
document.getElementById('modalConfirm').onclick = () => doAuth();
document.getElementById('authPassword').addEventListener('keydown', e => { if (e.key === 'Enter') doAuth(); });
document.getElementById('authCode').addEventListener('keydown', e => { if (e.key === 'Enter') doAuth(); });
@@ -691,13 +733,21 @@ if ($currentVersion !== $BUILD_VERSION) {
async function doPasswordAuth() {
const password = document.getElementById('authPassword').value;
if (!password) { document.getElementById('authError').textContent = '请输入密码'; return; }
document.getElementById('authError').textContent = '等待人机验证完成...';
let cap;
try {
cap = await cgPayload(); // ★ 人机验证 token(config.php 单开关,未启用时为空对象)
} catch (e) {
document.getElementById('authError').textContent = e.message || '请先完成人机验证';
return;
}
document.getElementById('authError').textContent = '正在提交登录请求...';
try {
// ★ 必须走web_login_request流程(Java验证密码),不能用verify_web_password(PHP直接验证=绕过Java)
const reqRes = await fetch(API + 'sync.php?action=web_login_request', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({player: currentPlayer, password: password})
body: JSON.stringify(Object.assign({player: currentPlayer, password: password}, cap))
});
const reqData = await reqRes.json();
if (!reqData.success) {
@@ -809,6 +859,7 @@ if ($currentVersion !== $BUILD_VERSION) {
style="width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px;outline:none;box-sizing:border-box">
</div>
<div id="loginStatusBox" style="display:none;margin-bottom:16px"></div>
<div id="cgSlotLogin"></div>
<button id="loginSubmitBtn" class="btn btn-primary" style="width:100%;padding:12px" onclick="doDirectLogin()">登录</button>
<p style="color:var(--dim);font-size:12px;margin-top:12px">密码由游戏服务器验证,Web端不存储密码</p>
</div>
@@ -821,6 +872,7 @@ if ($currentVersion !== $BUILD_VERSION) {
</div>
<button class="btn" onclick="showGuestMode()" style="padding:10px 20px;font-size:13px">👁️ 先逛逛(游客模式)</button>
</div>`;
cgMount('cgSlotLogin'); // ★ 挂载人机验证(唯一实例)
document.querySelector('.sidebar').style.display = 'none';
// 自动聚焦密码框
setTimeout(() => { const pwd = document.getElementById('loginPassword'); if (pwd) pwd.focus(); }, 100);
@@ -837,6 +889,15 @@ if ($currentVersion !== $BUILD_VERSION) {
if (!password) { showLoginStatus('请输入密码', 'error'); return; }
submitBtn.disabled = true;
showLoginStatus('等待人机验证完成...', 'loading');
let cap;
try {
cap = await cgPayload(); // ★ 人机验证 token(config.php 单开关,未启用时为空对象)
} catch (e) {
showLoginStatus(e.message || '请先完成人机验证', 'error');
submitBtn.disabled = false;
return;
}
showLoginStatus('正在提交登录请求...', 'loading');
try {
@@ -844,7 +905,7 @@ if ($currentVersion !== $BUILD_VERSION) {
const reqRes = await fetch(API + 'sync.php?action=web_login_request', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({player, password})
body: JSON.stringify(Object.assign({player, password}, cap))
});
const reqData = await reqRes.json();
if (!reqData.success) {
@@ -933,6 +994,7 @@ if ($currentVersion !== $BUILD_VERSION) {
function openGlassLogin() {
const overlay = document.getElementById('glassLoginOverlay');
overlay.classList.add('show');
cgMount('cgSlotGlass'); // ★ 挂载人机验证(唯一实例)
glassLoginDismissed = false;
// 隐藏C位登录按钮
document.querySelector('.guest-center-login').classList.remove('show');
@@ -987,12 +1049,21 @@ if ($currentVersion !== $BUILD_VERSION) {
if (!player) { setGlassStatus('请输入玩家名', 'error'); return; }
if (!password) { setGlassStatus('请输入密码', 'error'); return; }
btn.disabled = true; btn.textContent = '登录中...';
setGlassStatus('等待人机验证完成...', 'loading');
let cap;
try {
cap = await cgPayload(); // ★ 人机验证 token(config.php 单开关,未启用时为空对象)
} catch (e) {
setGlassStatus(e.message || '请先完成人机验证', 'error');
btn.disabled = false; btn.textContent = '登 录';
return;
}
setGlassStatus('正在提交登录请求...', 'loading');
try {
const reqRes = await fetch(API + 'sync.php?action=web_login_request', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({player, password})
body: JSON.stringify(Object.assign({player, password}, cap))
});
const reqData = await reqRes.json();
if (!reqData.success) { setGlassStatus(reqData.message || '提交失败', 'error'); btn.disabled = false; btn.textContent = '登 录'; return; }