第三方认证
第三方账户走 Yggdrasil 协议(authlib-injector 一系)。用户在账户页填一个认证服务器地址和账号密码,启动器换一张票,然后用 -javaagent: 把 authlib-injector 塞进 JVM,让游戏本身也走这台认证服务器。
仓库里没有内置任何第三方认证服务
认证服务器地址完全来自用户输入,代码里不硬编码任何一家。这是合规要求,也是产品要求——内置某个站点等于替它背书,也等于把用户引到我们无法审计的服务上。密码只作为方法参数传递,不落盘、不写日志。
解析服务器地址
用户填的可能是 example.org,也可能是 https://example.org/api/yggdrasil。先补协议头,然后 GET 一次这个地址:
- 响应是 JSON 且含
serverLocation→ 这是 authlib-injector 约定的别名跳转,以serverLocation的值为真实 base 继续解析 - 否则把它当作 Yggdrasil API 根,从
meta.serverName取展示名
别名跳转最多跟 3 跳,再多就报「认证服务器别名跳转超过 3 层,疑似配置错误」——防止 A 指 B、B 指 A 的死循环把用户锁死。
for (var hop = 0; hop < 3; hop++)
{
// ...
if (doc.RootElement.TryGetProperty("serverLocation", out var loc) &&
loc.GetString() is { Length: > 0 } real)
{
url = real;
continue;
}
return new YggServer(url.TrimEnd('/'), serverName);
}
throw new InvalidOperationException("认证服务器别名跳转超过 3 层,疑似配置错误");解析出来的 base 和用户原始填的地址分别记在账户的 server_base 和 server_alias 两个字段里——前者是真实 API 根,后者是用户看到的那个。
换票
POST {base}/authserver/authenticate
Content-Type: application/json{
"agent": { "name": "Minecraft", "version": 1 },
"username": "...",
"password": "...",
"clientToken": "...",
"requestUser": false
}agent.name 必须正好是 "Minecraft"
协议规范硬性要求 agent.name 等于 Minecraft。写成自家产品名("MinecraftLauncher")会被皮肤站一类的服务端直接拒绝,LittleSkin 的原话是 agent.name must be equal to Minecraft。这不是「不太标准但能过」——是直接 400。
agent.version 我们写 1;规范允许省略或写实现版本号,但既然 name 是写死的,version 也别自作主张写产品版本。
clientToken 是设备标识,不是鉴权 token——首次生成后终身固定复用。严格校验的认证服务器会直接拒收非 UUID 格式的 clientToken,所以我们按规范写成带连字符的标准 UUID(Guid.NewGuid().ToString())。
响应里有两种取角色名的方式:优先 selectedProfile,没有就取 availableProfiles[0](单角色服务器)。两个都没有就报「该账号在认证服务器上没有 Minecraft 角色」。
拿到的 accessToken 同样只进内存会话。
皮肤地址
GET {base}/sessionserver/session/minecraft/profile/{uuid}响应里的 properties 数组中,name == "textures" 的那一项,value 是 Base64,解出来是 JSON:
{ "textures": { "SKIN": { "url": "https://..." } } }取 textures.SKIN.url 就是这个角色在皮肤站上的纹理地址。
这里失败是可以接受的
查皮肤失败一律返回 null,不抛异常。皮肤是尽力而为的功能,正版/离线账户没有会话服务器可问也照样能启动游戏。为了一张缩略图让整个登录流程失败是本末倒置。
注入 authlib-injector
光拿到 token 不够——游戏进程自己也要连这台认证服务器,否则进游戏会被判定为非法会话。做法是给 JVM 挂一个 Java agent:
-javaagent:<path>/authlib-injector-1.2.8.jar=<serverBase>jar 随启动器发布在 Assets/authlib-injector/ 下,路径由 App 层注入给 Core(Core 不感知打包布局)。参数里 = 右边是认证服务器 base,不是账户 UUID——agent 自己去该服务器问「这个 token 对应谁」。
// authlib-injector:第三方(Yggdrasil)外置登录注入,必须出现在 -cp 之前
if (!string.IsNullOrWhiteSpace(options.AgentJarPath) && !string.IsNullOrWhiteSpace(options.AgentBase))
jvmArgs.Add($"-javaagent:{options.AgentJarPath}={options.AgentBase}");顺序有讲究:-javaagent 必须在 -cp 之前。JVM 参数是顺序敏感的,agent 放晚了它就来不及在类加载器初始化时挂钩。
第三方账户启动时还会带上 --userType legacy(正版是 msa,离线是 legacy 且不带 agent)。
agent 缺失要明确报错
如果 Assets/authlib-injector/ 下的 jar 缺失,启动器不能「降级成普通启动」——那样会以离线身份进游戏,用户以为登上了第三方账号其实没有。正确做法是抛「authlib-injector 组件缺失,无法启动第三方账户,请重新安装启动器」。