前言
Minecraft Mojang 账户迁移至 Microsoft 微软账户似乎已经近乎尾声,新的微软账户需要新的正版验证方式。这里我们将讨论整套的验证流程,并研究其使用 Python 实现的可能性。
Python 实现的原理是使用 Python 编写 Minecraft 启动器,由启动器接管正版验证。
本文仅探讨使用 Python 实现正版验证的流程,不涉及启动器的核心原理。
本教程使用到的 Python 库
requests 与 PySide2(或 PyQt5)
开始
在浏览器窗口登录
首先,第一步是需要玩家登陆他本人的微软账户。为了方便玩家在这个页面进行更多操作,我们需要创建一个浏览器窗口,在其中加载微软账户的登录页面。PyQt 和 PySide 提供可用的浏览器窗口,您需要做的是在这个窗口中加载下面这个 URL:
https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&response_type=code&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf
请不要修改这其中的任何内容,这是登录 Minecraft 专用的页面。您可以在自己的浏览器中尝试访问这个 URL,进行登录之后,您应当被重定向到另一个空白页面,其 URL 应为:
https://login.live.com/oauth20_desktop.srf?code=<code>&lc=<不重要>
我们需要做的,就是提取这个重定向 URL 中的 <code>
,这就是第一步。
如下的 Python 代码可以实现这个步骤:
def auth_microsoft_1():
"""
验证玩家的 Microsoft 账户 Minecraft 游戏凭证,并且配置玩家的正版登录。这是第一步。
:return: 此函数不返回值。
"""
global web_view
auth_url_1 = "https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&response_type=code&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL &redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf"
# 使用 web 窗口打开微软账户登录页面
web_view = FormAuth()
web_view.browser = QWebEngineView()
web_view.browser.load(QUrl(auth_url_1))
web_view.setCentralWidget(web_view.browser)
web_view.browser.urlChanged.connect(auth_microsoft_2)
web_view.show()
logger.Logger.info(f"微软账户登录窗口已经打开。")
return None
注意,虽然此代码被写在我的某个启动器项目中,但只有这样的代码并不能真正创建一个可见的窗口。如果您想要直接使用此代码,请使用搜索引擎学习如何添加 QApplication(sys.argv)
。另外,FormAuth()
是一个自定义的 Widget 窗口类,auth_microsoft_2
是连接到第二步的绑定函数。
auth_microsoft_2
函数中进行检测,只有重定向到正确的 URL 时进行下一步。关闭窗口,开始验证
重定向并获取到 code
之后,接下来您需要使用这个 code
获取微软授权令牌 access_token
。此后的步骤不再需要浏览器窗口,其一是因为不再需要用户确认任何信息,另外是防止一些不必要的用户隐私泄露。
下一步,向 https://login.live.com/oauth20_token.srf
发起 POST 请求,负载格式如下:
{
"client_id": "00000000402b5328",
"code": <code>,
"grant_type": "authorization_code",
"redirect_uri": "https://login.live.com/oauth20_desktop.srf",
"scope": "service::user.auth.xboxlive.com::MBI_SSL"
}
在 code
对应的值中装载上一步获取的 code
,其它的值不做改变;在请求头中设置 Content-Type: application/x-www-form-urlencoded
,然后发送请求。
响应看起来应当如下:
{
"token_type":"bearer",
"expires_in":86400,
"scope":"service::user.auth.xboxlive.com::MBI_SSL",
"access_token":<access_token>,
"refresh_token":<refresh_token>,
"user_id":"889ed4a3d844f672",
"foci":"1"
}
从响应中提取 access_token
和 refresh_token
两个令牌,前一个令牌应该比后一个令牌长一点(就一点,假的,长了好多)
access_token
是进行下一步验证所需要的微软验证令牌,我们现在已经通过了微软的身份验证。下一步,是向 Xbox live 进行身份验证,这也是微软的服务之一。验证 URL 为 https://user.auth.xboxlive.com/user/authenticate
,您需要向其发送 POST 请求,负载如下:
{
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": access_token // 第二步中获取的访问令牌
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}
不要忘记设置 Content-Type: application/json
;帆帆测试时没有设置 Accept: application/json
也能进行验证,如果您只接收到奇奇怪怪的返回值和返回代码,不妨加上试试。
如果没有异常,您应当收到如下响应:
{
"IssueInstant":"2020-12-07T19:52:08.4463796Z",
"NotAfter":"2020-12-21T19:52:08.4463796Z",
"Token":token, // 保存它,这是你的 Xbox live 令牌
"DisplayClaims":{
"xui":[
{
"uhs":"uhs" // 保存它,这是一个用户信息哈希值
}
]
}
}
保存 token
和 uhs
(是 user hash 的意思),然后开始下一步验证。
下一步的验证方是 XSTS,XSTS 会返回我们可用的 XSTS 令牌(没错,还是一个中间令牌)。验证 URL 是 https://xsts.auth.xboxlive.com/xsts/authorize
,依然是 POST 请求,负载如下:
{
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
token // 你上一步中获取的 token 就填在这儿
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}
同样设置 Content-Type: application/json
,然后发送,响应应该如下:
{
"IssueInstant":"2020-12-07T19:52:09.2345095Z",
"NotAfter":"2020-12-08T11:52:09.2345095Z",
"Token":token, // 保存它,这是你的 XSTS 令牌
"DisplayClaims":{
"xui":[
{
"uhs":"uhs" // 与上个请求相同
}
]
}
}
现在,我们终于可以回归到 Minecraft 本身上了。通过 XSTS 令牌和 uhs
,我们可以获取到用于启动游戏的 Minecraft 访问令牌。接下来让我们转向 https://api.minecraftservices.com/authentication/login_with_xbox
,向其发送 POST 请求,附上一个简洁干净的负载:
{
"identityToken": "XBL3.0 x=<uhs>;<xsts_token>" // 在这里添加之前获取的 uhs 和 XSTS 令牌
}
您应当收到如下响应:
{
"username" : "some uuid", // 这并不是该用户的 UUID
"roles" : [ ],
"access_token" : minecraft_access_token, // JWT,可以使用的 Minecraft 访问令牌
!
"token_type" : "Bearer",
"expires_in" : 86400
}
保存这个 minecraft_access_token
,然后 —— 不要着急,直到现在为止我们都还没有获取这个微软账户拥有的 Minecraft 用户名 —— 我们甚至没有检查其是否真的拥有 Minecraft 呢!
接下来的步骤继续需要我们向 Minecraft 进行验证,向 https://api.minecraftservices.com/entitlements/mcstore
发送一个 GET 请求,把 minecraft_access_token
塞到请求头里:
Authorization: Bearer token // 只有 token 是你获取的 Minecraft 访问令牌,Bearer 不要动它,不然会报 401!
如果对应的账户拥有游戏资格,则会返回一堆东西;如果不然,则返回空负载。
检测到返回了有效的内容之后,我们就可以继续获取这个账户的 Minecraft 用户名和 UUID 用来启动游戏。向 https://api.minecraftservices.com/minecraft/profile
发送一个 GET 请求,使用与上一步同样的请求头,接收到如下负载:
{
"id" : UUID, // 账号的真实 UUID
"name" : PlayerName, // 该账号的 Minecraft 用户名
"skins" : [ {
"id" : "6a6e65e5-76dd-4c3c-a625-162924514568",
"state" : "ACTIVE",
"url" : url, // 指向一个纹理,应该是皮肤的图片文件
"variant" : "CLASSIC",
"alias" : "STEVE"
} ],
"capes" : [ ]
}
从中提取 name
和 id
两个值,其他的值可选。现在,我们就已经获得了启动 Minecraft 所需的全部信息。
以下是整个步骤的 Python 实现,使用了 requests
网络库。
def auth_microsoft_2():
"""
验证微软账户第二步。
:return: 此函数不返回值。
"""
global web_view
url: str = web_view.browser.url().toString()
if not "https://login.live.com/oauth20_desktop.srf?code=" in url:
return None
web_view.close()
logger.Logger.debug(f"微软账户登录 - 完成,重定向地址为:{url}")
temp_code = url.replace("https://login.live.com/oauth20_desktop.srf?code=", "").split("&")[0]
auth_url_2 = "https://login.live.com/oauth20_token.srf"
temp_data_1 = {"client_id": "00000000402b5328",
"code": temp_code,
"grant_type": "authorization_code",
"redirect_uri": "https://login.live.com/oauth20_desktop.srf",
"scope": "service::user.auth.xboxlive.com::MBI_SSL"}
rq_1 = rq.post(auth_url_2,
data=temp_data_1,
headers={'Content-Type': 'application/x-www-form-urlencoded'})
back_json_1 = rq_1.content
logger.Logger.debug(f"微软账户登录 - 完成,微软 Oauth 回传 JSON 为:{back_json_1} | {rq_1.url} | {rq_1.status_code}")
access_token = json.loads(back_json_1)["access_token"]
auth_url_3 = "https://user.auth.xboxlive.com/user/authenticate"
temp_data_2 = {
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": access_token
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}
temp_data_2 = json.dumps(temp_data_2) # 必须进行此步操作否则 Xbox live 一定会 400,我也不知道为什么好离谱!
rq_2 = rq.post(auth_url_3,
data=temp_data_2,
headers={"Content-Type": "application/json"},
)
back_json_2 = rq_2.content
logger.Logger.debug(f"微软账户登录 - 完成,Xbox live 回传 JSON 为:{back_json_2} | {rq_2.url} | {rq_2.status_code}")
back_json_2_ = json.loads(back_json_2)
token_1 = back_json_2_["Token"]
uhs_1 = back_json_2_["DisplayClaims"]["xui"][0]["uhs"]
# 验证 XSTS 令牌
auth_url_4 = "https://xsts.auth.xboxlive.com/xsts/authorize"
temp_data_3 = {
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
token_1
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}
temp_data_3 = json.dumps(temp_data_3)
rq_3 = rq.post(auth_url_4,
data=temp_data_3,
headers={"Content-Type": "application/json"},
)
back_json_3 = rq_3.content
logger.Logger.debug(f"微软账户登录 - 完成,XSTS 验证回传 JSON 为:{back_json_3} | {rq_3.url} | {rq_3.status_code}")
back_json_3_ = json.loads(back_json_3)
token_2 = back_json_3_["Token"]
uhs_2 = back_json_3_["DisplayClaims"]["xui"][0]["uhs"]
if uhs_1 != uhs_2:
logger.Logger.warning("微软账户登录 - 错误,Xbox live 与 XSTS 回传的用户 uhs 不一致,登录取消。")
raise
auth_url_5 = "https://api.minecraftservices.com/authentication/login_with_xbox"
temp_data_4 = {
"identityToken": f"XBL3.0 x={uhs_1};{token_2}"
}
temp_data_4 = json.dumps(temp_data_4)
rq_4 = rq.post(auth_url_5,
data=temp_data_4,
)
back_json_4 = rq_4.content
logger.Logger.debug(f"微软账户登录 - 完成,Minecraft API Xbox 验证回传 JSON 为:{back_json_4} | {rq_4.url} | {rq_4.status_code}")
minecraft_access_token = json.loads(back_json_4)["access_token"]
auth_url_6 = "https://api.minecraftservices.com/entitlements/mcstore"
rq_5 = rq.get(auth_url_6,
headers={"Authorization": f"Bearer {minecraft_access_token}"}
)
back_json_5 = rq_5.content
logger.Logger.debug(f"微软账户登录 - 完成,Minecraft API 玩家游戏凭证回传 JSON 为:{back_json_5} | {rq_5.url} | {rq_5.status_code}")
if not back_json_5:
logger.Logger.warning("微软账户登录 - 错误,该账户似乎并不拥有 Minecraft,登录取消。")
raise
auth_url_7 = "https://api.minecraftservices.com/minecraft/profile"
rq_6 = rq.get(auth_url_7,
headers={"Authorization": f"Bearer {minecraft_access_token}"}
)
back_json_6 = rq_6.content
logger.Logger.debug(f"微软账户登录 - 完成,Minecraft API 玩家信息回传 JSON 为:{back_json_6} | {rq_6.url} | {rq_6.status_code}")
back_json_6_ = json.loads(back_json_6)
player_uuid = back_json_6_["id"]
player_name = back_json_6_["name"]
player = MinecraftPlayerTools(player_name).auth_microsoft(minecraft_access_token, player_uuid)
logger.Logger.info(f"微软账户登录 - 完成,玩家{player.player_name}({player.player_uuid})通过微软账户验证,MinecraftPlayerTools 对象已经建立。")
return None
附注
我是个菜鸟,别骂我(抱头就跑 啊 头没了)
本人 Python 目前只有入门水平,这边给的代码也跑不起来(因为你没有我这边的一整个环境,我各种引用外置库搞得乱七八糟的(捂脸))
过阵子我会把这个项目传到 GitHub 或者 Gitee 上,然后大家可以去研究,现在先将就着看一看吧。
欸嘿~(再次抱头就跑)
验证流程中有一处获取了 refresh_token
,这个令牌可以用于刷新对应的 access_token
的有效期,从而确保最终的 minecraft_access_token
的有效性,在外部链接第一条中已有详细描述,这里不再说明。
外部链接
Zh:Microsoft Authentication Scheme – wiki.vg
请登录后查看评论内容