minecraft_auth.php: 改用公共客户端ID实现OAuth,新增verify_code和paste_code_page端点

This commit is contained in:
ypsdf1
2026-07-17 14:02:38 +08:00
parent d539cc10bf
commit ff83406013
+211 -19
View File
@@ -103,6 +103,20 @@ function generateSessionId() {
* 向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,
@@ -110,16 +124,27 @@ function msPost($url, $data, $headers = []) {
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_HTTPHEADER => array_merge([
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json',
], $headers),
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"];
}
@@ -144,6 +169,15 @@ function msGet($url, $headers = []) {
$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"];
}
@@ -156,13 +190,15 @@ function msGet($url, $headers = []) {
/**
* 步骤1: 生成Microsoft授权URL
* ★ 使用Minecraft官方启动器的公共Client ID(00000000402b5328)
* 此Client ID自带Xbox Live和Minecraft API权限,无需申请
* 缺点:redirect_uri必须是login.live.com的桌面端页面,玩家需手动复制授权码
*/
function getAuthUrl($sessionId) {
$clientId = MS_CLIENT_ID;
$redirectUri = MS_REDIRECT_URI;
// ★ 使用Microsoft Graph的User.Read scope(不依赖Xbox Live API权限)
// Xbox Live token交换会在后端用此token完成
$scope = 'User.Read offline_access';
$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,
@@ -170,6 +206,7 @@ function getAuthUrl($sessionId) {
'scope' => $scope,
'redirect_uri' => $redirectUri,
'state' => $sessionId,
'prompt' => 'select_account',
]);
return "https://login.live.com/oauth20_authorize.srf?$params";
@@ -177,14 +214,20 @@ function getAuthUrl($sessionId) {
/**
* 步骤2: 用授权码交换Access Token
* ★ 公共客户端(00000000402b5328)不需要client_secret
* redirect_uri必须与授权时一致
*/
function exchangeToken($code) {
$clientId = '00000000402b5328'; // Minecraft公共客户端ID
// 桌面端redirect_uri(与getAuthUrl一致)
$redirectUri = 'https://login.live.com/oauth20_desktop.srf';
$result = msPost('https://login.live.com/oauth20_token.srf', [
'client_id' => MS_CLIENT_ID,
'client_secret' => MS_CLIENT_SECRET,
'client_id' => $clientId,
'code' => $code,
'grant_type' => 'authorization_code',
'redirect_uri' => MS_REDIRECT_URI,
'redirect_uri' => $redirectUri,
'scope' => 'XboxLive.signin offline_access',
]);
if (isset($result['error'])) {
@@ -207,13 +250,19 @@ function exchangeToken($code) {
/**
* 步骤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' => $msAccessToken,
'RpsTicket' => $rpsTicket,
],
'RelyingParty' => 'http://auth.xboxlive.com',
'TokenType' => 'JWT',
@@ -222,21 +271,26 @@ function getXboxToken($msAccessToken) {
'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']];
return ['error' => "Xbox auth HTTP {$result['code']}: " . json_encode($result['body'] ?? [])];
}
$body = $result['body'];
if (!isset($body['Token'])) {
return ['error' => 'No Xbox token', 'response' => $body];
return ['error' => 'No Token in response: ' . json_encode($body)];
}
return [
'token' => $body['Token'],
'uhs' => $body['IssueClaims']['uhs'] ?? '',
'uhs' => $body['IssueClaims']['uhs'] ?? ($body['DisplayClaims']['xui'][0]['uhs'] ?? ''),
];
}
@@ -403,9 +457,9 @@ switch ($action) {
if (empty($player)) error('缺少player参数');
if ($secret !== MC_AUTH_SECRET) error('密钥错误', 403);
// 检查MS_CLIENT_SECRET是否配置
if (empty(MS_CLIENT_SECRET)) {
error('Microsoft OAuth未配置:缺少MS_CLIENT_SECRET,请在pay_secrets.php中设置');
// 检查MS_CLIENT_ID是否配置(公共客户端不需要secret)
if (empty(MS_CLIENT_ID)) {
error('Microsoft OAuth未配置:缺少MS_CLIENT_ID,请在pay_secrets.php中设置');
}
// 清理该玩家的旧会话
@@ -426,6 +480,7 @@ switch ($action) {
success([
'session_id' => $sessionId,
'auth_url' => $authUrl,
'paste_url' => 'https://caoyuan.ypshidifu.cn/plugin/api/minecraft_auth.php?action=paste_code_page&session_id=' . $sessionId,
'expires_in' => 600,
]);
break;
@@ -556,6 +611,56 @@ switch ($action) {
exit;
}
// ===== 玩家手动粘贴授权码(公共客户端流程) =====
case 'verify_code': {
// 兼容JSON和表单两种POST方式
$json = json_decode(file_get_contents('php://input'), true);
$sessionId = postParam('session_id') ?: ($json['session_id'] ?? '');
$code = postParam('code') ?: ($json['code'] ?? '');
$secret = postParam('secret') ?: ($json['secret'] ?? '');
if (empty($sessionId)) error('缺少session_id参数');
if (empty($code)) error('缺少code参数(授权码)');
// 密钥验证:Java调用需要secret,浏览器表单用session_id自身认证
if (!empty($secret)) {
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') 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流程
$result = completeAuthFlow($code);
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');
@@ -595,6 +700,93 @@ switch ($action) {
break;
}
// ===== 网页粘贴授权码页面(公共客户端流程) =====
case 'paste_code_page': {
$sessionId = getParam('session_id');
if (empty($sessionId)) {
echo "<!DOCTYPE html><html><head><meta charset='utf-8'><title>参数错误</title></head><body><h1>缺少session_id参数</h1></body></html>";
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 "<!DOCTYPE html><html><head><meta charset='utf-8'><title>会话不存在</title></head><body><h1>会话不存在或已过期</h1></body></html>";
exit;
}
$playerName = htmlspecialchars($session['player_name']);
echo <<<HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Minecraft正版验证 - 粘贴授权码</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;background:#0f0f23;color:#e0e0e0}
.card{background:#1a1a3e;padding:40px;border-radius:16px;text-align:center;max-width:420px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.4)}
h2{margin-bottom:8px;color:#4ade80}
.player{color:#888;font-size:14px;margin-bottom:24px}
.steps{text-align:left;background:#0f0f23;border-radius:8px;padding:16px;margin-bottom:24px;font-size:14px;line-height:1.8}
.steps li{margin-left:20px;color:#bbb}
.steps li b{color:#e0e0e0}
.input-group{margin-bottom:16px}
input[type=text]{width:100%;padding:14px 16px;font-size:16px;font-family:monospace;background:#0f0f23;border:2px solid #333;border-radius:8px;color:#4ade80;text-align:center;letter-spacing:2px;outline:none;transition:border-color 0.2s}
input[type=text]:focus{border-color:#4ade80}
button{width:100%;padding:14px;font-size:16px;font-weight:bold;background:#4ade80;color:#0f0f23;border:none;border-radius:8px;cursor:pointer;transition:all 0.2s}
button:hover{background:#22c55e;transform:translateY(-1px)}
button:disabled{opacity:0.5;cursor:not-allowed;transform:none}
.msg{margin-top:16px;font-size:14px;min-height:20px}
.msg.ok{color:#4ade80}
.msg.err{color:#f87171}
</style>
</head>
<body>
<div class="card">
<h2>🎮 正版验证</h2>
<p class="player">玩家: <b>$playerName</b></p>
<ol class="steps">
<li>在上方Microsoft页面<b>复制授权码</b></li>
<li>将授权码<b>粘贴</b>到下方输入框</li>
<li>点击"提交验证"</li>
</ol>
<div class="input-group">
<input type="text" id="codeInput" placeholder="粘贴授权码到这里" autofocus autocomplete="off">
</div>
<button id="submitBtn" onclick="submitCode()">提交验证</button>
<div class="msg" id="msg"></div>
</div>
<script>
var sessionId="{$sessionId}";
function submitCode(){
var code=document.getElementById('codeInput').value.trim();
if(!code){showMsg('请输入授权码','err');return}
var btn=document.getElementById('submitBtn');
btn.disabled=true;btn.textContent='验证中...';
var xhr=new XMLHttpRequest();
xhr.open('POST','minecraft_auth.php?action=verify_code',true);
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.onreadystatechange=function(){
if(xhr.readyState===4){
try{var d=JSON.parse(xhr.responseText);
if(d.success){showMsg('✅ 验证成功!请关闭此页面返回游戏','ok');}
else{showMsg('❌ '+(d.error||'验证失败'),'err');btn.disabled=false;btn.textContent='提交验证';}
}catch(e){showMsg('❌ 服务器响应异常','err');btn.disabled=false;btn.textContent='提交验证';}
}};
xhr.send('session_id='+encodeURIComponent(sessionId)+'&code='+encodeURIComponent(code));
}
function showMsg(t,c){var m=document.getElementById('msg');m.textContent=t;m.className='msg '+c;}
</script>
</body>
</html>
HTML;
exit;
}
default:
error('未知action: ' . $action);
}