作者:UpSGE
前言
本文研究了从启动游戏到加入服务器的验证过程(在线模式),并给出了相关的应用示例。
验证过程
服务端启动阶段
Velocity
- 生成
服务器公钥
Vanilla
- 生成
服务器公钥 - 通过 公钥 API 获取
Mojang公钥
启动器登入阶段
- 通过登入响应获取
AccessToken、UUID和Username
客户端启动阶段
- 启动器将
AccessToken、UUID和Username使用启动参数传递给客户端 - 客户端通过 公钥 API 获取
Mojang公钥
加入服务器阶段
- 客户端生成
共享密钥,服务端生成随机serverId - 客户端与服务器交换
共享密钥、Username、服务器公钥和serverId - 客户端和服务端使用
serverId、共享密钥和服务器公钥计算服务器哈希 - 客户端将
AccessToken、UUID和服务器哈希发送到 Join API ,若AccessToken和UUID验证通过,服务器返回204响应,否则客户端提示无效会话 - 服务端将
Username、服务器哈希发送到 hasJoined API ,若Username和服务器哈希验证通过,则返回服务端UUID、服务端Username和签名材质,否则客户端提示尚未登入至 Minecraft 账户(该提示可由服务端自定义) - 服务端将
服务端UUID、服务端Username发给客户端,客户端加入服务器,服务端将签名材质广播给所有玩家
后续
Vanilla 服务端验证玩家档案公钥签名
- 客户端发送
CHAT_SESSION_UPDATE包 - 服务端使用
Mojang公钥验证玩家档案公钥签名 - 若验证不通过,服务端将玩家踢出服务器,客户端提示
无效的玩家档案公钥签名
客户端加载皮肤
- 客户端接收到
签名材质广播 - 客户端使用
Mojang公钥验证签名材质是否有效,并判断材质 URL 是否在白名单内 - 若验证不通过,客户端将不加载该玩家的皮肤
离线模式
客户端
- 启动器传递一个无效的
AccessToken给客户端
服务端
- 客户端加入服务器时,服务器不再要求客户端发送
Join API请求,直接让客户端加入服务器 服务端UUID由Username生成- 不广播皮肤
- 不验证玩家档案公钥签名
应用分析
- 注意到
Join API和hasJoined API是两个独立的 API,且是否允许进服最终取决于hasJoined API的验证结果。通过改变hasJoined API端点,API 可以控制玩家是否可以加入服务器。 - 服务器中的实际
服务端UUID与客户端的UUID无关,仅从hasJoined API中获取。这可以用于实现玩家身份的映射。 - 玩家在服务器中显示的皮肤完全由服务端决定,服务端可以通过提供有效的
签名材质来控制玩家的皮肤显示。 - 通过改变客户端与服务端的各种 API 端点,可以实现私有的玩家档案管理系统。
相关项目
- MUA Union:让一个服务器能让不同认证源登入的客户端加入,并正确显示皮肤
- MultiJoin:让 Velocity 能让不同认证源登入的客户端加入,并提供 UUID 映射与绑定
- JustEnoughSkins:通过提供有效的签名材质让来自不同认证源登入的客户端的玩家之间能相互看到皮肤
- Accounts X:通过修改客户端的各种 API 端点,实现游戏内切换玩家档案
- ChatSessionBlocker:通过阻断
CHAT_SESSION_UPDATE包传递到后端服务器,防止因为玩家档案公钥签名验证失败而导致玩家被踢出服务器 - NoChatReports:在客户端层面阻断
CHAT_SESSION_UPDATE包 - Authlib Injector:一站式解决外置登入问题,并提供 Yggdrasil 技术规范。
- Blessing Skin Server:实现了 Yggdrasil 技术规范和皮肤库
- Drasl:同上,Go 实现
- Element Skin:同上,Python + Vue 实现