Skip to content

启动器技术规范

Haowei Wen edited this page Jun 25, 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 地址,启动器需要进行如下操作:

  1. 如果 URL 缺少协议,则将其补全为 HTTPS 协议。
  2. 向 URL 发送 GET 请求(跟随 HTTP 重定向)。
  3. 如果响应包含 ALI 头(HTTP 头部 X-Authlib-Injector-API-Location),那么 ALI 指向的 URL 就是 API 地址。
    • X-Authlib-Injector-API-Location 可以是绝对 URL,亦可以是相对 URL。
    • 如果 ALI 指向其自身,那就意味着当前 URL 就是 API 地址。
  4. 如果响应不包含 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 和用户属性。

账户的添加

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

此后,启动器进行以下操作:

  1. 调用相应验证服务器的登录接口,其中包含用户输入的账户和密码。
  2. 如果响应中 selectedProfile 不为空,则登录成功,使用响应中的信息更新账户属性。流程结束。
  3. 如果响应中 availableProfiles 为空,则用户没有任何角色,触发异常。
  4. 提示用户从 availableProfiles 中选择一个角色。
  5. 调用刷新接口,其中的令牌为登录操作所返回的令牌,selectedProfile 为上一步中用户选择的角色。
  6. 登录成功,使用刷新响应中的信息更新账户属性。

凭证有效性的确认

启动器在使用凭证前(例如启动游戏前),需要确认其有效性。如果凭证失效,则需要用户重新登录。确认凭证有效性的步骤如下:

  1. 调用验证令牌接口,其中包含账户的 accessToken 和 clientToken。
  2. 如果请求成功,则当前凭证有效,流程结束。否则继续执行。
  3. 调用刷新接口,其中包含账户的 accessToken 和 clientToken。
  4. 如果请求成功,则使用刷新响应中的信息更新账户属性,流程结束。否则继续执行。
  5. 启动器要求用户重新输入密码。
  6. 调用登录接口,其中包含用户的账户和上一步输入的密码。
  7. 如果登录响应中 selectedProfile 不为空,则:
    1. 如果 selectedProfile 中的 uuid 与账户对应角色的 UUID 相同,则使用登录响应中的信息更新用户属性。流程结束。
    2. 触发异常(原账户的角色已不可用)。
  8. availableProfiles 中找出 UUID 与账户对应角色相同的角色。如果没有,则触发异常(原账户的角色已不可用)。
  9. 调用刷新接口,其中令牌为登录操作返回的令牌,selectedProfile 为上一步中所找到的角色。
  10. 登录成功,使用刷新响应中的信息更新账户属性。

账户信息的显示

启动器在显示账户时,除了账户对应角色的名称之外,还应该显示账户所属的验证服务器(见上文),防止用户混淆不同验证服务器上的同名角色。

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

启动游戏

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

  1. (若需要)下载 authlib-injector
  2. 确认凭证有效性
  3. 配置预获取
  4. 添加启动参数

其中第 1、2、3 步可以并行执行,以提升启动速度。

下载 authlib-injector

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

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

配置预获取

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

添加启动参数

配置 authlib-injector

启动器需要添加以下 JVM 参数(应加在主类参数前):

  1. javaagent 参数:
    -javaagent:{authlib-injector.jar 的路径}={验证服务器 API 地址}
    
  2. 配置预获取:
    -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 格式)(若启动器不支持,可替换为 {}