启动器技术规范
Haowei Wen edited this page on Jun 26, 2022 · 32 revisions
概述
本文旨在为启动器实现 authlib-injector 规范提供技术指导。由于该功能需要调用 Yggdrasil API，因此建议您在阅读本文前，先阅读 Yggdrasil 服务端技术规范。

在启动器中，本登录方式可以被称为【外置登录（authlib-injector）】或【authlib-injector 登录】。我们推荐您使用含义更为明确的前者。

验证服务器
验证服务器（即 Yggdrasil 服务器）是整个验证系统的核心，所有验证相关的请求都将被发往它。

为了确定一个验证服务器，启动器应当存储该验证服务器的 API 地址（即 API Root，如 https://example.com/api/yggdrasil/）。

启动器可以仅支持一个验证服务器，也可以支持多个验证服务器。支持多验证服务器就意味着，启动器中可以同时存在多个账户，并且这些账户可以属于不同的验证服务器。

验证服务器的设置
设置验证服务器的操作一般由玩家完成，但也存在着服主进行设置、配置文件同启动器和游戏一起分发的情况。下面介绍几种设置验证服务器的途径：

在配置文件中指定
启动器可以将 API 地址直接存储于配置文件中，让用户通过修改配置文件的方式设置验证服务器。这种配置方式实现简单，如果你的启动器只用作服务器专用启动器，则可以使用这种配置方式。

在启动器中输入地址
在这种配置方式下，用户通过在启动器中输入 URL 来完成验证服务器的设置。这里的 URL 可能是完整的 API 地址（如 https://example.com/api/yggdrasil/），也可能是缩略的地址（如 example.com）。

当 URL 未标明协议（HTTPS 或 HTTP）时，我们约定将其自动补全为 HTTPS 协议。亦即，example.com/api/yggdrasil/ 应当被理解为 https://example.com/api/yggdrasil/。

出于安全考虑，启动器即使无法通过 HTTPS 协议连接，也不得降级到明文的 HTTP 协议。

同时，authlib-injector 规定了一种服务发现机制，称为 API 地址指示（ALI）。它用于将用户输入的缩略的、不完整的地址，转换为完整的 API 地址。

处理 API 地址指示（ALI）
为了将用户输入的地址解析为真正的 API 地址，启动器需要进行如下操作：

如果 URL 缺少协议，则将其补全为 HTTPS 协议。
向 URL 发送 GET 请求（跟随 HTTP 重定向）。
如果响应包含 ALI 头（HTTP 头部 X-Authlib-Injector-API-Location），那么 ALI 指向的 URL 就是 API 地址。
X-Authlib-Injector-API-Location 可以是绝对 URL，亦可以是相对 URL。
如果 ALI 指向其自身，那就意味着当前 URL 就是 API 地址。
如果响应不包含 ALI 头部，则默认认为当前 URL 是 API 地址。
伪代码：

function resolve_api_url(url)
    response = http_get(url) // follow redirects

    if response.headers["x-authlib-injector-api-location"] exists
        new_url = to_absolute_url(response.headers["x-authlib-injector-api-location"])
        if new_url != url
            return new_url

    // if you are going to fetch the metadata next, 'response' can be reused
    return url
通过拖拽设置
此方式允许用户通过鼠标拖拽 (DnD)来设置验证服务器。

DnD 源可以是浏览器或者其他应用程序，而 DnD 目标则是启动器。DnD 源需要展示一段文字、图片或其他内容，示意用户将此内容拖入启动器，以添加验证服务端。在此过程中，验证服务器信息从 DnD 源传输到启动器。在完成 DnD 动作后，启动器向用户确认是否要添加此验证服务器。

拖动数据
拖动数据的 MIME 类型为 text/plain，内容为一段 URI，其格式如下：

authlib-injector:yggdrasil-server:{验证服务端的 API 地址}
其中的 API 地址是 URI 的一个组成，应当被编码。

拖动效果为复制（copy）。

HTML 示例
演示页面

在需要拖动的 DOM 节点上添加 draggable="true"，并处理 dragstart 事件：

<span id="dndLabel" draggable="true" ondragstart="dndLabel_dragstart(event);">example.yggdrasil.yushi.moe</span>
function dndLabel_dragstart(event) {
	let yggdrasilApiRoot = "https://example.yggdrasil.yushi.moe/";
	let uri = "authlib-injector:yggdrasil-server:" + encodeURIComponent(yggdrasilApiRoot);
	event.dataTransfer.setData("text/plain", uri);
	event.dataTransfer.dropEffect = "copy";
}
验证服务器信息的呈现
通过向 API 地址发送 GET 请求，启动器可以获取到验证服务器的元数据（响应格式），例如服务器名称。启动器可以利用这些元数据提升用户体验。

服务器名称的显示
验证服务器在 meta 中的 serverName 里指定了验证服务器的名称。当启动器需要向用户显示一个验证服务器时，便可以使用该名称。

需要注意的是，验证服务器名称可能会出现冲突，因此启动器应当提供查看验证服务器 API 地址的方法。例如，当鼠标悬浮在验证服务器名称上时，启动器在 Tooltip 中显示其 API 地址。

对非 HTTPS 验证服务器的警告
当用户尝试设置一个使用明文 HTTP 协议的验证服务器时，启动器应向用户显示醒目警告，告知用户这可能将其信息安全置于危险之中，用户的密码将会被明文传输。

账户
一个账户对应了游戏中的一个玩家，用户可以在启动时选择一个账户进行游戏。

账户、用户和角色的关系：启动器中账户的概念，与验证服务器中用户的概念并不相同。与启动器中的账户相对应的，是验证服务器中的角色（Profile）。验证服务器中的用户则是一个或多个角色的所有者，其在启动器中并无对应实体。

账户信息的存储
启动器通过以下三个不可变的属性来确定一个账户：

账户所属验证服务器
账户的标识（如邮箱）
通常账户标识即为邮箱。但若验证服务器返回的元数据中 feature.non_email_login 字段为 true，则表明验证服务器支持使用邮箱以外的凭证登录，即账户标识可以不是邮箱。此时，启动器不应当期望用户输入的账户一定是邮箱，同时应注意措辞（如使用「账户」一词代替「邮箱」一词），以免造成迷惑。（详见 Yggdrasil 服务端技术规范 § 使用角色名称登录）
账户所对应角色的 UUID
仅当两个账户的以上三个属性都相同时，这两个账户才是相同的，其中一个属性相同并不能代表两账户相同。同一验证服务器上可以存在多个角色；多个角色可以属于同一个用户；不同验证服务器上也可以出现具有相同 UUID 的角色。因此，启动器应同时使用这三个属性来标识账户。

除了以上三个属性外，账户还具有以下属性：

令牌（accessToken 及 clientToken）
账户所对应角色的名称
用户的 ID
用户的属性
安全警告：

记住登录状态记录的是令牌，不是用户的密码。密码在任何时候都不应该被明文存储。
以上属性都是可变的。每次登录或刷新操作后，启动器都需要更新存储的账户属性。

在下面所有的登录和刷新操作中，请求中 requestUser 参数都为 true，这样启动器便能够即时更新用户 ID 和用户属性。

账户的添加
如果用户要添加一个账户，则启动器需要询问用户使用的验证服务器、用户的账户和密码。这里的验证服务器，可以是预先设置好的，可以是用户从验证服务器列表中选择的，也可以是用户即时设置的（见上文）。

此后，启动器进行以下操作：

调用相应验证服务器的登录接口，其中包含用户输入的账户和密码。
如果响应中 selectedProfile 不为空，则登录成功，使用响应中的信息更新账户属性。流程结束。
如果响应中 availableProfiles 为空，则用户没有任何角色，触发异常。
提示用户从 availableProfiles 中选择一个角色。
调用刷新接口，其中的令牌为登录操作所返回的令牌，selectedProfile 为上一步中用户选择的角色。
登录成功，使用刷新响应中的信息更新账户属性。
凭证有效性的确认
启动器在使用凭证前（例如启动游戏前），需要确认其有效性。如果凭证失效，则需要用户重新登录。确认凭证有效性的步骤如下：

调用验证令牌接口，其中包含账户的 accessToken 和 clientToken。
如果请求成功，则当前凭证有效，流程结束。否则继续执行。
调用刷新接口，其中包含账户的 accessToken 和 clientToken。
如果请求成功，则使用刷新响应中的信息更新账户属性，流程结束。否则继续执行。
启动器要求用户重新输入密码。
调用登录接口，其中包含用户的账户和上一步输入的密码。
如果登录响应中 selectedProfile 不为空，则：
如果 selectedProfile 中的 uuid 与账户对应角色的 UUID 相同，则使用登录响应中的信息更新用户属性。流程结束。
触发异常（原账户的角色已不可用）。
从 availableProfiles 中找出 UUID 与账户对应角色相同的角色。如果没有，则触发异常（原账户的角色已不可用）。
调用刷新接口，其中令牌为登录操作返回的令牌，selectedProfile 为上一步中所找到的角色。
登录成功，使用刷新响应中的信息更新账户属性。
账户信息的显示
启动器在显示账户时，除了账户对应角色的名称之外，还应该显示账户所属的验证服务器（见上文），防止用户混淆不同验证服务器上的同名角色。

如果启动器要显示角色皮肤，则可以调用查询角色属性接口获取角色属性，角色属性中包含了角色的皮肤信息。

启动游戏
启动器在启动游戏前需要进行以下工作：

（若需要）下载 authlib-injector
确认凭证有效性
配置预获取
添加启动参数
其中第 1、2、3 步可以并行执行，以提升启动速度。

下载 authlib-injector
启动器可以自带 authlib-injector.jar，也可以在启动游戏前下载一个（并缓存）。本项目提供了一个 API 用于下载 authlib-injector。

如果你的用户主要在中国大陆，我们推荐你从 BMCLAPI 镜像下载。

配置预获取
在启动前，启动器需要向 API 地址发送 GET 请求，获取 API 元数据。该元数据会在启动时被传入游戏，这样 authlib-injector 就不必直接请求验证服务器，进而能提升启动速度，并防止因网络故障而导致的游戏启动时崩溃。

添加启动参数
配置 authlib-injector
启动器需要添加以下 JVM 参数（应加在主类参数前）：

javaagent 参数：
-javaagent:{authlib-injector.jar 的路径}={验证服务器 API 地址}
配置预获取：
-Dauthlibinjector.yggdrasil.prefetched={Base64 编码的 API 元数据}
下面以 example.yggdrasil.yushi.moe 为例：

authlib-injector.jar 位于 /home/user/.launcher/authlib-injector.jar。
验证服务器的 API 地址为 https://example.yggdrasil.yushi.moe/。
向 https://example.yggdrasil.yushi.moe/ 发送 GET 请求，获取到 API 元数据：
{"skinDomains":["yushi.moe"],"signaturePublickey":...（省略）
对上面的响应进行 Base64 编码，得到：
eyJza2luRG9tYWluc...（省略）
故添加的 JVM 参数为：
-javaagent:/home/user/.launcher/authlib-injector.jar=https://example.yggdrasil.yushi.moe/
-Dauthlibinjector.yggdrasil.prefetched=eyJza2luRG9tYWluc...（省略）
替换参数模板
游戏版本 JSON 文件（versions/<version>/<version>.json）中规定了启动器启动游戏时使用的参数，其中部分与验证相关的参数模板应按下表进行替换：

参数模板	替换为
${auth_access_token}	账户的 accessToken
${auth_session}	账户的 accessToken
${auth_player_name}	角色的名称
${auth_uuid}	角色的 UUID（无符号）
${user_type}	mojang
${user_properties}	用户的属性（JSON 格式）（若启动器不支持，可替换为 {}）
Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
