fix: 永久封禁玩家禁止登录Web后台、正版验证粘贴页响应异常时回查真实状态

- core.php 新增 playerWebBanReason():查 web_player_bans 名字封禁(永久/未过期),expire_time 兼容Java推送的毫秒单位;validateWebAccess 接入,商城/CDK/余额/token校验一律拒绝
- api/sync.php 四个登录入口全部拦截:validate_weblogin_token、web_login_request、send_email_code、verify_email_code
- api/minecraft_auth.php:success/error 清空输出缓冲保证纯JSON;新增 action=status 会话真实状态查询;粘贴页提交响应异常时回查状态,已验证则如实提示通过,不再误报"服务器响应异常"
This commit is contained in:
ypsdf1
2026-09-28 00:03:07 +08:00
parent eaadc0bd64
commit 4db131082c
3 changed files with 115 additions and 4 deletions
+50 -3
View File
@@ -64,11 +64,15 @@ if (mcAuthIpBlacklisted()) {
// ====================================================================
function success($data = null) {
// ★ 清空所有输出缓冲,保证响应体是纯JSON(否则前端 JSON.parse 失败会误报"服务器响应异常")
while (ob_get_level() > 0) { @ob_end_clean(); }
echo json_encode(['success' => true, 'data' => $data], JSON_UNESCAPED_UNICODE);
exit;
}
function error($msg, $code = 400) {
// ★ 清空所有输出缓冲,保证响应体是纯JSON
while (ob_get_level() > 0) { @ob_end_clean(); }
http_response_code($code);
echo json_encode(['success' => false, 'error' => $msg], JSON_UNESCAPED_UNICODE);
exit;
@@ -599,6 +603,29 @@ switch ($action) {
break;
}
// ===== 粘贴页/前端轮询:查询会话真实状态(仅凭session_id)=====
// 用途:前端提交响应异常(非JSON/网关错误)时回查真实结果,避免"已验证通过却提示响应异常"的误导
case 'status': {
$sessionId = getParam('session_id');
if (empty($sessionId)) error('缺少session_id参数');
$stmt = $db->prepare("SELECT * FROM mc_auth_sessions WHERE session_id = ?");
$stmt->execute([$sessionId]);
$session = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$session) error('会话不存在或已过期');
$result = [
'status' => $session['status'],
'player_name' => $session['player_name'],
];
if ($session['status'] === 'verified') {
$result['mc_uuid'] = $session['mc_uuid'];
$result['mc_username'] = $session['mc_username'];
}
success($result);
break;
}
// ===== 浏览器回调:处理Microsoft OAuth =====
case 'callback': {
$code = getParam('code');
@@ -899,6 +926,24 @@ if(ph.has('code'))return p.get('code');
}catch(e){}
return input;
}
function enableBtn(){var btn=document.getElementById('submitBtn');btn.disabled=false;btn.textContent='提交验证';}
function showRealStatus(httpStatus,raw){
// ★ 提交响应异常时回查会话真实状态:已验证就如实告知"已通过",不再误导为"服务器响应异常"
var x2=new XMLHttpRequest();
x2.open('GET','minecraft_auth.php?action=status&session_id='+encodeURIComponent(sessionId),true);
x2.onreadystatechange=function(){
if(x2.readyState!==4)return;
var st=null;
try{var d2=JSON.parse(x2.responseText); if(d2.success&&d2.data)st=d2.data.status;}catch(e){}
var rawPrev=String(raw||'').replace(/\s+/g,' ').substring(0,160);
if(st==='verified'){showMsg('✅ 服务器已验证通过(提交时响应异常已自动忽略),请关闭此页面返回游戏','ok');}
else if(st==='failed'){showMsg('❌ 验证失败,请重新提交 (HTTP '+httpStatus+')','err');enableBtn();}
else if(st==='expired'){showMsg('❌ 会话已过期,请回游戏重新发起验证','err');enableBtn();}
else if(st==='pending'){showMsg('⚠️ 响应异常(HTTP '+httpStatus+'),服务器仍在验证中,请稍后再次点击提交…','err');enableBtn();}
else{showMsg('❌ 服务器响应异常(HTTP '+httpStatus+'): '+rawPrev,'err');enableBtn();}
};
x2.send();
}
function doSubmit(code){
if(submitting)return;
submitting=true;
@@ -910,10 +955,12 @@ xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.onreadystatechange=function(){
if(xhr.readyState===4){
submitting=false;
try{var d=JSON.parse(xhr.responseText);
var body='';
try{body=xhr.responseText||'';}catch(e){}
try{var d=JSON.parse(body);
if(d.success){showMsg('✅ 验证成功!请关闭此页面返回游戏','ok');}
else{showMsg('❌ '+(d.error||'验证失败'),'err');btn.disabled=false;btn.textContent='提交验证';}
}catch(e){showMsg('❌ 服务器响应异常','err');btn.disabled=false;btn.textContent='提交验证';}
else{showMsg('❌ '+(d.error||d.message||'验证失败'),'err');enableBtn();}
}catch(e){showRealStatus(xhr.status,body);}
}};
xhr.send('session_id='+encodeURIComponent(sessionId)+'&code='+encodeURIComponent(code));
}
+25 -1
View File
@@ -2001,6 +2001,12 @@ function apiValidateWebloginToken() {
error('登录Token已过期');
}
// ★ 封禁拦截:被封禁玩家禁止登录Web后台(永久封禁/未过期封禁均拦截)
$banReason = playerWebBanReason($row['player_name'], $db);
if ($banReason !== null) {
error('账号已被封禁:' . $banReason);
}
success(['player' => $row['player_name']], 'Token有效');
} catch (\Throwable $e) {
error('Token验证失败: ' . $e->getMessage());
@@ -2379,6 +2385,12 @@ function webLoginRequest() {
$db = getDB();
// ★ 封禁拦截:被封禁玩家禁止走密码登录
$banReason = playerWebBanReason($player, $db);
if ($banReason !== null) {
error('账号已被封禁:' . $banReason);
}
// 检查玩家是否已注册
if (!isPlayerRegistered($player)) {
error('玩家未注册,请先在游戏中注册');
@@ -3421,6 +3433,12 @@ function sendEmailCode() {
error('玩家未在游戏中注册,请先在游戏中使用 /register 注册账号');
}
// ★ 封禁拦截:被封禁玩家禁止发送登录验证码
$banReason = playerWebBanReason($player);
if ($banReason !== null) {
error('账号已被封禁:' . $banReason);
}
// ★ 从数据库读取玩家绑定的邮箱(唯一可信来源)
$db = getDB();
$stmt = $db->prepare("SELECT email FROM users WHERE player_name = :player");
@@ -3526,9 +3544,15 @@ function verifyEmailCode() {
$tokenInfo = validateTokenSilent($webToken);
if (!$tokenInfo) error('无效的登录token');
// 验证玩家名是否匹配
// ★ 验证玩家名是否匹配
if ($tokenInfo['player'] !== $player) error('玩家名不匹配');
// ★ 封禁拦截:被封禁玩家禁止走邮箱验证码登录
$banReason = playerWebBanReason($player);
if ($banReason !== null) {
error('账号已被封禁:' . $banReason);
}
// ★ 从数据库读取玩家绑定的邮箱(唯一可信来源)
$db = getDB();
$stmt = $db->prepare("SELECT email FROM users WHERE player_name = :player");
+40
View File
@@ -1431,6 +1431,39 @@ function isPlayerRegistered($playerName) {
* @param string $password 用户输入的密码(关键操作时需要)
* @param string $ipAddress 客户端IP
*/
/**
* ★ 封禁拦截:查询玩家是否处于【名字封禁】生效期(Java /ban 通过 sync_bans 同步的 web_player_bans)
* 返回 null = 未封禁(或数据不可用时放行);否则返回封禁原因字符串(供前端提示)。
* expire_time 单位兼容:Java 推送的是毫秒(BanEntry.getExpiration().getTime()),0 = 永久。
* 覆盖范围:token登录 / 密码登录 / 邮箱验证码登录 / 商城·CDK·余额等 validateWebAccess 入口。
*/
function playerWebBanReason($player, $db = null) {
if ($player === null || $player === '') return null;
try {
if ($db === null) $db = getDB();
// 表不存在 = 从未同步过封禁名单 → 直接放行(避免读路径执行DDL)
$exists = $db->querySingle("SELECT name FROM sqlite_master WHERE type='table' AND name='web_player_bans'");
if (!$exists) return null;
$stmt = $db->prepare("SELECT reason, expire_time FROM web_player_bans WHERE ban_type = 'name' AND LOWER(target) = LOWER(:p)");
if (!$stmt) return null;
$stmt->bindValue(':p', $player, SQLITE3_TEXT);
$res = $stmt->execute();
if (!$res) return null;
$now = time();
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
$exp = (int)$row['expire_time'];
if ($exp > 100000000000) $exp = intdiv($exp, 1000); // 毫秒 → 秒
if ($exp === 0 || $exp > $now) {
$reason = trim((string)($row['reason'] ?? ''));
return $reason !== '' ? $reason : '违反服务器规则';
}
}
} catch (\Throwable $e) {
@error_log('[playerWebBanReason] ' . $e->getMessage());
}
return null;
}
function validateWebAccess($webToken, $action = 'view', $password = null, $ipAddress = null) {
if (!$ipAddress) $ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
@@ -1459,6 +1492,13 @@ function validateWebAccess($webToken, $action = 'view', $password = null, $ipAdd
$playerName = $row['player_name'];
// ★ 封禁拦截:被封禁玩家禁止使用Web端(商城/CDK/余额/登录校验一律拒绝)
$banReason = playerWebBanReason($playerName, $db);
if ($banReason !== null) {
@error_log("[validateWebAccess] DENIED: player banned. player=$playerName");
return ['ok' => false, 'mode' => 'denied', 'message' => '账号已被封禁:' . $banReason];
}
// 在线玩家表(Java插件同步,这里确保表存在)
try {
$db->exec("CREATE TABLE IF NOT EXISTS online_players (player_name TEXT PRIMARY KEY, login_time INTEGER DEFAULT 0)");