setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $st = $w->prepare("SELECT 1 FROM web_ip_blacklist WHERE ip = ? AND (expire = 0 OR expire > ?) LIMIT 1"); $st->execute([$ip, time()]); if (!$st->fetch()) return false; // 未被封 → 放行 // 被封 → 必须持游戏内 /web 签发的有效token $token = isset($_REQUEST['web_token']) ? (string)$_REQUEST['web_token'] : ''; if ($token === '') return true; $t = $w->prepare("SELECT created_at, expire_seconds FROM weblogin_tokens WHERE web_token = ? LIMIT 1"); $t->execute([$token]); $row = $t->fetch(PDO::FETCH_ASSOC); return !($row && (time() - (int)$row['created_at']) <= (int)$row['expire_seconds']); } catch (\Throwable $e) { return false; // 任何异常 fail-open,避免误伤 } } if (mcAuthIpBlacklisted()) { http_response_code(403); echo json_encode(['success' => false, 'blocked_ip' => true, 'message' => '该IP已被服务器封禁,需在游戏内执行 /web 获取授权token后才能访问'], JSON_UNESCAPED_UNICODE); exit; } // ==================================================================== // 工具函数 // ==================================================================== 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; } function getParam($key, $default = '') { return isset($_GET[$key]) ? trim($_GET[$key]) : $default; } function postParam($key, $default = '') { return isset($_POST[$key]) ? trim($_POST[$key]) : $default; } /** * 获取SQLite数据库连接(orders.db) */ function getDb() { $possiblePaths = [ dirname(__DIR__) . '/data/orders.db', dirname(__DIR__) . '/orders.db', dirname(__DIR__) . '/../data/orders.db', dirname(__DIR__) . '/../orders.db', ]; foreach ($possiblePaths as $path) { if (file_exists($path)) { $db = new PDO('sqlite:' . $path); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->exec('PRAGMA journal_mode=WAL'); return $db; } } // 都不存在,创建在第一个路径 $dir = dirname($possiblePaths[0]); if (!is_dir($dir)) @mkdir($dir, 0755, true); $db = new PDO('sqlite:' . $possiblePaths[0]); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->exec('PRAGMA journal_mode=WAL'); return $db; } /** * 确保mc_auth_sessions表存在 */ function ensureTable($db) { $db->exec("CREATE TABLE IF NOT EXISTS mc_auth_sessions ( session_id TEXT PRIMARY KEY, player_name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', mc_uuid TEXT DEFAULT '', mc_username TEXT DEFAULT '', mc_access_token TEXT DEFAULT '', ip_address TEXT DEFAULT '', created_at INTEGER NOT NULL, verified_at INTEGER DEFAULT 0, expires_at INTEGER NOT NULL )"); } /** * 生成随机session_id */ function generateSessionId() { return bin2hex(random_bytes(16)); } // ==================================================================== // Microsoft OAuth 工具函数 // ==================================================================== /** * 向Microsoft发送POST请求 */ function msPost($url, $data, $headers = []) { // 检查调用者是否已指定Content-Type $hasContentType = false; foreach ($headers as $h) { if (stripos($h, 'Content-Type:') === 0) { $hasContentType = true; break; } } $defaultHeaders = ['Accept: application/json']; if (!$hasContentType) { $defaultHeaders[] = 'Content-Type: application/x-www-form-urlencoded'; } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => is_array($data) ? http_build_query($data) : $data, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_HTTPHEADER => array_merge($defaultHeaders, $headers), ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); // 详细日志记录 $log = date('Y-m-d H:i:s') . "\n"; $log .= "URL: $url\n"; if (is_array($data)) { $log .= "Data: " . http_build_query($data) . "\n"; } else { $log .= "Data: $data\n"; } $log .= "HTTP Code: $httpCode\n"; $log .= "cURL Error: " . ($error ?: 'none') . "\n"; $log .= "Response: " . substr($response, 0, 500) . "\n"; $log .= str_repeat('-', 80) . "\n"; @file_put_contents(__DIR__ . '/debug_minecraft.log', $log, FILE_APPEND); if ($error) { return ['error' => "cURL error: $error"]; } return ['code' => $httpCode, 'body' => json_decode($response, true)]; } /** * 向Microsoft发送GET请求 */ function msGet($url, $headers = []) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_HTTPHEADER => array_merge([ 'Accept: application/json', ], $headers), ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); // 详细日志记录 $log = date('Y-m-d H:i:s') . "\n"; $log .= "GET URL: $url\n"; $log .= "HTTP Code: $httpCode\n"; $log .= "cURL Error: " . ($error ?: 'none') . "\n"; $log .= "Response: " . substr($response, 0, 500) . "\n"; $log .= str_repeat('-', 80) . "\n"; @file_put_contents(__DIR__ . '/debug_minecraft.log', $log, FILE_APPEND); if ($error) { return ['error' => "cURL error: $error"]; } return ['code' => $httpCode, 'body' => json_decode($response, true)]; } // ==================================================================== // 核心OAuth流程 // ==================================================================== /** * 步骤1: 生成Microsoft授权URL * ★ 使用Minecraft官方启动器的公共Client ID(00000000402b5328) * 此Client ID自带Xbox Live和Minecraft API权限,无需申请 * 缺点:redirect_uri必须是login.live.com的桌面端页面,玩家需手动复制授权码 */ function getAuthUrl($sessionId) { $clientId = '00000000402b5328'; // Minecraft官方启动器公共客户端ID // 桌面端专用redirect_uri(Microsoft展示授权码的页面) $redirectUri = 'https://login.live.com/oauth20_desktop.srf'; $scope = 'XboxLive.signin offline_access'; $params = http_build_query([ 'client_id' => $clientId, 'response_type' => 'code', 'scope' => $scope, 'redirect_uri' => $redirectUri, 'state' => $sessionId, 'prompt' => 'select_account', ]); return "https://login.live.com/oauth20_authorize.srf?$params"; } /** * 步骤2: 用授权码交换Access Token * ★ 公共客户端(00000000402b5328)不需要client_secret * redirect_uri必须与授权时一致 */ /** * 将Microsoft返回的OAuth错误映射为中文友好提示 */ function mapMsTokenError($msErr, $msDesc = '') { $msDesc = trim($msDesc); if ($msErr === 'invalid_grant') { return '授权码已失效(可能已过期,或已被使用过一次)。请重新点击"打开Microsoft登录"获取新的授权码,并在短时间内完成粘贴提交,不要重复使用旧授权码。'; } if ($msErr === 'invalid_client') { return 'OAuth客户端验证失败,请联系管理员检查 MS_CLIENT_ID 配置。'; } if ($msErr === 'invalid_request') { return '请求参数有误(' . ($msDesc ?: 'invalid_request') . '),请联系管理员。'; } return 'Token兑换失败: ' . $msErr . ($msDesc ? '(' . $msDesc . ')' : ''); } function exchangeToken($code) { $clientId = '00000000402b5328'; // Minecraft公共客户端ID // 桌面端redirect_uri(与getAuthUrl一致) $redirectUri = 'https://login.live.com/oauth20_desktop.srf'; // ★ 关键修复:对code做一次urldecode归一化。 // 前端JS提取code后会encodeURIComponent(! → %21), // 若此处不解码直接交给http_build_query,PHP会再次编码成 %2521(双重编码), // 导致Microsoft收到失真的授权码 → invalid_grant 400。 // 做一次urldecode保证最终只编码一次。urldecode对已是明文的内容是幂等的,安全。 $code = urldecode($code); $result = msPost('https://login.live.com/oauth20_token.srf', [ 'client_id' => $clientId, 'code' => $code, 'grant_type' => 'authorization_code', 'redirect_uri' => $redirectUri, 'scope' => 'XboxLive.signin offline_access', ]); if (isset($result['error'])) { return ['error' => 'Token exchange failed: ' . $result['error']]; } if ($result['code'] !== 200) { $body = $result['body'] ?? []; $msErr = $body['error'] ?? 'unknown'; $msDesc = $body['error_description'] ?? ''; return ['error' => mapMsTokenError($msErr, $msDesc), 'ms_error' => $msErr]; } $body = $result['body']; if (!isset($body['access_token'])) { return ['error' => 'No access_token in response', 'response' => $body]; } return [ 'access_token' => $body['access_token'], 'refresh_token' => $body['refresh_token'] ?? '', ]; } /** * 步骤3: 获取Xbox Live Token * ★ RpsTicket格式确认:必须用 "d= {token}" 前缀(来自日志验证) */ function getXboxToken($msAccessToken) { $rpsTicket = 'd=' . $msAccessToken; $log = date('Y-m-d H:i:s') . "\n"; $log .= "=== Xbox Token (d=前缀) ===\n"; $result = msPost('https://user.auth.xboxlive.com/user/authenticate', json_encode([ 'Properties' => [ 'AuthMethod' => 'RPS', 'SiteName' => 'user.auth.xboxlive.com', 'RpsTicket' => $rpsTicket, ], 'RelyingParty' => 'http://auth.xboxlive.com', 'TokenType' => 'JWT', ]), [ 'Content-Type: application/json', 'Accept: application/json', ]); $log .= "HTTP Code: " . ($result['code'] ?? 'N/A') . "\n"; $log .= "Response: " . json_encode($result['body'] ?? $result['error'] ?? 'N/A') . "\n"; $log .= str_repeat('-', 80) . "\n"; @file_put_contents(__DIR__ . '/debug_minecraft.log', $log, FILE_APPEND); if (isset($result['error'])) { return ['error' => 'Xbox auth failed: ' . $result['error']]; } if ($result['code'] !== 200) { return ['error' => "Xbox auth HTTP {$result['code']}: " . json_encode($result['body'] ?? [])]; } $body = $result['body']; if (!isset($body['Token'])) { return ['error' => 'No Token in response: ' . json_encode($body)]; } return [ 'token' => $body['Token'], 'uhs' => $body['IssueClaims']['uhs'] ?? ($body['DisplayClaims']['xui'][0]['uhs'] ?? ''), ]; } /** * 步骤4: 获取XSTS Token */ function getXstsToken($xboxToken, $uhs) { $result = msPost('https://xsts.auth.xboxlive.com/xsts/authorize', json_encode([ 'Properties' => [ 'SandboxId' => 'RETAIL', 'UserTokens' => [$xboxToken], ], 'RelyingParty' => 'rp://api.minecraftservices.com/', 'TokenType' => 'JWT', ]), [ 'Content-Type: application/json', 'Accept: application/json', ]); if (isset($result['error'])) { return ['error' => 'XSTS auth failed: ' . $result['error']]; } if ($result['code'] !== 200) { $body = $result['body'] ?? []; $errCode = $body['XErr'] ?? 'unknown'; return ['error' => "XSTS auth HTTP {$result['code']}, XErr: {$errCode}", 'response' => $body]; } $body = $result['body']; if (!isset($body['Token'])) { return ['error' => 'No XSTS token', 'response' => $body]; } return [ 'token' => $body['Token'], 'uhs' => $body['IssueClaims']['uhs'] ?? $uhs, ]; } /** * 步骤5: 获取Minecraft Access Token */ function getMinecraftToken($xstsToken, $uhs) { $result = msPost('https://api.minecraftservices.com/authentication/login_with_xbox', json_encode([ 'identityToken' => "XBL3.0 x={$uhs};{$xstsToken}", ]), [ 'Content-Type: application/json', 'Accept: application/json', ]); if (isset($result['error'])) { return ['error' => 'MC token failed: ' . $result['error']]; } if ($result['code'] !== 200) { return ['error' => 'MC token HTTP ' . $result['code'], 'response' => $result['body']]; } $body = $result['body']; if (!isset($body['access_token'])) { return ['error' => 'No MC access_token', 'response' => $body]; } return ['access_token' => $body['access_token']]; } /** * 步骤6: 获取Minecraft Profile */ function getMinecraftProfile($mcAccessToken) { $result = msGet('https://api.minecraftservices.com/minecraft/profile', [ 'Authorization: Bearer ' . $mcAccessToken, ]); if (isset($result['error'])) { return ['error' => 'MC profile failed: ' . $result['error']]; } if ($result['code'] === 404) { return ['error' => 'No Minecraft profile found (account may not own the game)']; } if ($result['code'] !== 200) { return ['error' => 'MC profile HTTP ' . $result['code'], 'response' => $result['body']]; } $body = $result['body']; if (!isset($body['id']) || !isset($body['name'])) { return ['error' => 'Invalid MC profile', 'response' => $body]; } // 格式化UUID(加横杠) $rawUuid = $body['id']; $formattedUuid = substr($rawUuid, 0, 8) . '-' . substr($rawUuid, 8, 4) . '-' . substr($rawUuid, 12, 4) . '-' . substr($rawUuid, 16, 4) . '-' . substr($rawUuid, 20, 12); return [ 'uuid' => $formattedUuid, 'name' => $body['name'], 'skins' => $body['skins'] ?? [], ]; } /** * 完整OAuth流程(从授权码到Minecraft Profile) */ function completeAuthFlow($code) { // 步骤2: 交换MS Token $tokenResult = exchangeToken($code); if (isset($tokenResult['error'])) return $tokenResult; // 步骤3: Xbox Live Token $xboxResult = getXboxToken($tokenResult['access_token']); if (isset($xboxResult['error'])) return $xboxResult; // 步骤4: XSTS Token $xstsResult = getXstsToken($xboxResult['token'], $xboxResult['uhs']); if (isset($xstsResult['error'])) return $xstsResult; // 步骤5: Minecraft Token $mcResult = getMinecraftToken($xstsResult['token'], $xstsResult['uhs']); if (isset($mcResult['error'])) return $mcResult; // 步骤6: Minecraft Profile $profileResult = getMinecraftProfile($mcResult['access_token']); if (isset($profileResult['error'])) return $profileResult; return [ 'uuid' => $profileResult['uuid'], 'name' => $profileResult['name'], 'access_token' => $mcResult['access_token'], ]; } // ==================================================================== // Action处理 // ==================================================================== // ★ 检测Microsoft OAuth回调(带code+state,没有action参数) $code = getParam('code'); $state = getParam('state'); $msError = getParam('error'); if (!empty($code) && !empty($state)) { // 自动路由到callback处理 $action = 'callback'; } elseif (!empty($msError)) { // Microsoft返回错误(如invalid_scope)→ 路由到callback显示错误 $action = 'callback'; } else { $action = getParam('action'); } $db = getDb(); ensureTable($db); switch ($action) { // ===== Java调用:创建验证会话 ===== case 'create_session': { $player = getParam('player'); $secret = getParam('secret'); $ip = $_SERVER['REMOTE_ADDR'] ?? ''; if (empty($player)) error('缺少player参数'); if ($secret !== MC_AUTH_SECRET) error('密钥错误', 403); // 检查MS_CLIENT_ID是否配置(公共客户端不需要secret) if (empty(MS_CLIENT_ID)) { error('Microsoft OAuth未配置:缺少MS_CLIENT_ID,请在pay_secrets.php中设置'); } // 清理该玩家的旧会话 $stmt = $db->prepare("DELETE FROM mc_auth_sessions WHERE player_name = ? AND status != 'verified'"); $stmt->execute([$player]); // 创建新会话 $sessionId = generateSessionId(); $now = time(); $expiresAt = $now + 600; // 10分钟过期 $stmt = $db->prepare("INSERT INTO mc_auth_sessions (session_id, player_name, status, ip_address, created_at, expires_at) VALUES (?, ?, 'pending', ?, ?, ?)"); $stmt->execute([$sessionId, $player, $ip, $now, $expiresAt]); // 生成授权URL $authUrl = getAuthUrl($sessionId); // ★ 动态构建paste_url:基于当前请求的域名和脚本路径,不硬编码 $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? ''; $scriptPath = $_SERVER['SCRIPT_NAME'] ?? '/api/minecraft_auth.php'; $pasteUrl = $scheme . '://' . $host . $scriptPath . '?action=paste_code_page&session_id=' . $sessionId; success([ 'session_id' => $sessionId, 'auth_url' => $authUrl, 'paste_url' => $pasteUrl, 'expires_in' => 600, ]); break; } // ===== Java调用:检查会话状态 ===== case 'check_session': { $sessionId = getParam('session_id'); $secret = getParam('secret'); if (empty($sessionId)) error('缺少session_id参数'); if ($secret !== MC_AUTH_SECRET) error('密钥错误', 403); // 查询会话 $stmt = $db->prepare("SELECT * FROM mc_auth_sessions WHERE session_id = ?"); $stmt->execute([$sessionId]); $session = $stmt->fetch(PDO::FETCH_ASSOC); if (!$session) { error('会话不存在'); } // 检查是否过期 if ($session['status'] === 'pending' && time() > $session['expires_at']) { $stmt = $db->prepare("UPDATE mc_auth_sessions SET status = 'expired' WHERE session_id = ?"); $stmt->execute([$sessionId]); $session['status'] = 'expired'; } $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']; $result['verified_at'] = $session['verified_at']; } elseif ($session['status'] === 'failed') { $result['error'] = '验证失败,请重试'; } success($result); 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'); $state = getParam('state'); // session_id $msError = getParam('error'); // Microsoft返回错误 if ($msError) { header('Content-Type: text/html; charset=utf-8'); $errorDesc = getParam('error_description', 'Microsoft登录被拒绝'); if ($state) { $stmt = $db->prepare("UPDATE mc_auth_sessions SET status = 'failed' WHERE session_id = ?"); $stmt->execute([$state]); } echo "验证失败
❌

验证失败

" . htmlspecialchars($errorDesc) . "

请关闭此页面并返回游戏重试

"; exit; } if (empty($code) || empty($state)) { header('Content-Type: text/html; charset=utf-8'); echo "参数错误

参数错误

"; exit; } // 查询会话 $stmt = $db->prepare("SELECT * FROM mc_auth_sessions WHERE session_id = ?"); $stmt->execute([$state]); $session = $stmt->fetch(PDO::FETCH_ASSOC); if (!$session) { header('Content-Type: text/html; charset=utf-8'); echo "会话不存在

会话不存在或已过期

"; exit; } if (time() > $session['expires_at']) { $stmt = $db->prepare("UPDATE mc_auth_sessions SET status = 'expired' WHERE session_id = ?"); $stmt->execute([$state]); header('Content-Type: text/html; charset=utf-8'); echo "会话过期

会话已过期,请返回游戏重新发起验证

"; exit; } // 执行完整OAuth流程 $result = completeAuthFlow($code); if (isset($result['error'])) { // 验证失败 $stmt = $db->prepare("UPDATE mc_auth_sessions SET status = 'failed' WHERE session_id = ?"); $stmt->execute([$state]); header('Content-Type: text/html; charset=utf-8'); echo "验证失败
❌

验证失败

" . htmlspecialchars($result['error']) . "

请关闭此页面并返回游戏重试

"; exit; } // 验证成功! $stmt = $db->prepare("UPDATE mc_auth_sessions SET status = 'verified', mc_uuid = ?, mc_username = ?, mc_access_token = ?, verified_at = ? WHERE session_id = ?"); $stmt->execute([$result['uuid'], $result['name'], $result['access_token'], time(), $state]); // 显示成功页面 $mcName = htmlspecialchars($result['name']); $mcUuid = htmlspecialchars($result['uuid']); header('Content-Type: text/html; charset=utf-8'); echo "验证成功
✅

正版验证成功!

$mcName
$mcUuid

请关闭此页面返回游戏

你的正版身份已验证,将自动登录服务器

"; exit; } // ===== 玩家手动粘贴授权码(公共客户端流程) ===== case 'verify_code': { // 兼容JSON和表单两种POST方式 $rawInput = file_get_contents('php://input'); $json = json_decode($rawInput, true); $sessionId = postParam('session_id') ?: ($json['session_id'] ?? ''); $code = postParam('code') ?: ($json['code'] ?? ''); $secret = postParam('secret') ?: ($json['secret'] ?? ''); // ★ 详细调试日志 $debugLine = date('Y-m-d H:i:s') . " [verify_code] 来源: " . (postParam('code') !== '' ? 'POST表单' : 'JSON体') . "\n"; $debugLine .= " Raw input(前300): " . substr($rawInput, 0, 300) . "\n"; $debugLine .= " Session: $sessionId\n"; $debugLine .= " Code(pre-extract,前200): " . substr($code, 0, 200) . " (len=" . strlen($code) . ")\n"; $debugLine .= " Secret: " . substr($secret, 0, 30) . "\n"; @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine, FILE_APPEND); if (empty($sessionId)) error('缺少session_id参数'); if (empty($code)) error('缺少code参数(授权码)'); // 自动从URL中提取code参数(兼容 ?code= & #code= & 直接粘贴code) if (strpos($code, 'code=') !== false || strpos($code, 'http') === 0 || strpos($code, '#') !== false) { if (preg_match('/[#?&]code=([^&]+)/', $code, $m)) { $code = urldecode($m[1]); } } // ★ 提取后日志 $debugLine2 = " Code(post-extract,前200): " . substr($code, 0, 200) . " (len=" . strlen($code) . ")\n"; @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine2, FILE_APPEND); // 密钥验证:Java调用需要secret,浏览器表单用session_id自身认证 if (!empty($secret)) { if ($secret !== MC_AUTH_SECRET) { $debugLine3 = " ★ 密钥不匹配! 收到: " . substr($secret, 0, 30) . " 期望: " . substr(MC_AUTH_SECRET, 0, 30) . "\n"; @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine3, FILE_APPEND); error('密钥错误', 403); } } // ★ 人机验证码(2026-10-02):只拦浏览器粘贴页的提交; // Java 插件带 secret 的调用走的是服务端,不需要人机验证。 if (empty($secret)) { cgEnforce('verify_code'); } // 查询会话 $stmt = $db->prepare("SELECT * FROM mc_auth_sessions WHERE session_id = ?"); $stmt->execute([$sessionId]); $session = $stmt->fetch(PDO::FETCH_ASSOC); if (!$session) { $debugLine4 = " ★ 会话不存在! sessionId=$sessionId\n"; @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine4, FILE_APPEND); error('会话不存在或已过期'); } if ($session['status'] !== 'pending') { $debugLine4 = " ★ 会话状态异常: " . $session['status'] . " (期望pending)\n"; @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine4, FILE_APPEND); error('会话状态异常: ' . $session['status']); } if (time() > $session['expires_at']) { $stmt = $db->prepare("UPDATE mc_auth_sessions SET status = 'expired' WHERE session_id = ?"); $stmt->execute([$sessionId]); error('会话已过期,请重新发起验证'); } // 执行完整OAuth流程 $debugLine5 = " → 调用 completeAuthFlow, code前50: " . substr($code, 0, 50) . "\n"; @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine5, FILE_APPEND); $result = completeAuthFlow($code); $debugLine6 = " ← completeAuthFlow返回: " . (isset($result['error']) ? 'ERROR: ' . $result['error'] : 'SUCCESS uuid=' . ($result['uuid'] ?? '?') . ' name=' . ($result['name'] ?? '?')) . "\n"; $debugLine6 .= str_repeat('-', 80) . "\n"; @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine6, FILE_APPEND); if (isset($result['error'])) { $stmt = $db->prepare("UPDATE mc_auth_sessions SET status = 'failed' WHERE session_id = ?"); $stmt->execute([$sessionId]); error('验证失败: ' . $result['error']); } // 验证成功 $stmt = $db->prepare("UPDATE mc_auth_sessions SET status = 'verified', mc_uuid = ?, mc_username = ?, mc_access_token = ?, verified_at = ? WHERE session_id = ?"); $stmt->execute([$result['uuid'], $result['name'], $result['access_token'], time(), $sessionId]); success([ 'status' => 'verified', 'mc_uuid' => $result['uuid'], 'mc_username' => $result['name'], ]); break; } // ===== Java调用:直接验证(用于验证Minecraft账号是否正版) ===== case 'verify_premium': { $player = getParam('player'); $secret = getParam('secret'); if (empty($player)) error('缺少player参数'); if ($secret !== MC_AUTH_SECRET) error('密钥错误', 403); // 通过Mojang API验证用户名是否对应正版账号 $url = "https://api.mojang.com/users/profiles/minecraft/" . urlencode($player); $result = msGet($url); if (isset($result['error'])) { error('Mojang API请求失败: ' . $result['error']); } if ($result['code'] === 200 && isset($result['body']['id'])) { $rawUuid = $result['body']['id']; $formattedUuid = substr($rawUuid, 0, 8) . '-' . substr($rawUuid, 8, 4) . '-' . substr($rawUuid, 12, 4) . '-' . substr($rawUuid, 16, 4) . '-' . substr($rawUuid, 20, 12); success([ 'is_premium' => true, 'uuid' => $formattedUuid, 'name' => $result['body']['name'] ?? $player, ]); } else { success([ 'is_premium' => false, 'uuid' => null, 'name' => $player, ]); } break; } // ===== 网页粘贴授权码页面(公共客户端流程) ===== case 'paste_code_page': { header('Content-Type: text/html; charset=utf-8'); $sessionId = getParam('session_id'); if (empty($sessionId)) { echo "参数错误

缺少session_id参数

"; exit; } // 验证session存在 $stmt = $db->prepare("SELECT * FROM mc_auth_sessions WHERE session_id = ?"); $stmt->execute([$sessionId]); $session = $stmt->fetch(PDO::FETCH_ASSOC); if (!$session) { echo "会话不存在

会话不存在或已过期

"; exit; } $playerName = htmlspecialchars($session['player_name']); $authUrl = htmlspecialchars(getAuthUrl($sessionId)); $captchaHtml = cgWidgetHtml(); echo << Minecraft正版验证 - 粘贴授权码

🎮 正版验证

玩家: $playerName

点击打开Microsoft登录
  1. 点击上方"打开Microsoft登录"按钮
  2. 登录Microsoft账号
  3. 登录成功后,复制浏览器地址栏的完整URL
  4. 将URL粘贴到下方输入框(会自动提取授权码)
  5. 点击"提交验证"
{$captchaHtml}
HTML; exit; } default: error('未知action: ' . $action); }