本端点为 Yggdrasil-服务端技术规范
的项目化实现,凡是兼容此规范的应用均能兼容本规范。
因此本目录下所有url路径、请求格式和相应格式都是不变的,但是为了方便理解和避免歧义,将一些原来的概念更改了名字。但本质没有改变。
注意:本目录下所有端点均独立于其他端点,如和其他端点冲突,请以本目录文件为准。除非特别说明, /yggdrasil 下所有端点不设 Cookie 身份验证,不设人机验证。
这里的 cause 一般不包含。
| 异常情况 | HTTP状态码 | Error | Error Message |
|---|---|---|---|
| 令牌无效 | 403 | ForbiddenOperationException | Invalid token. |
| 密码错误,或短时间内多次登录失败而被暂时禁止登录 | 403 | ForbiddenOperationException | Invalid credentials. Invalid username or password. |
| 试图向一个已经绑定了角色的令牌指定其要绑定的角色 | 400 | IllegalArgumentException | Access token already has a profile assigned. |
| 试图向一个令牌绑定不属于其对应用户的角色 (非标准) | 403 | ForbiddenOperationException | 未定义 |
| 试图使用一个错误的角色加入服务器 | 403 | ForbiddenOperationException | Invalid token. |
此项将存储于数据库数据表中。
一个系统中可以存在若干个启动器会话,启动器会话具有以下属性:
ID(启动器会话ID)邮箱(启动器会话登录名) 生成格式为 6位随机短无符号UUID@example.com(可通过配置更改域名),也可通过配置改为 用户displayName__角色名(详见 创建启动器会话 的备注),默认第一种。密码(启动器会话认证凭据) 随机生成固定长度的无符号 UUID,长度可通过配置更改(6-12),默认 6。其中 ID(启动器会话ID) 为一个无符号 UUID。ID(启动器会话ID) 和 邮箱(启动器会话登录名) 不可变更,且需要保证唯一。
实际上,启动器会话应该绑定且只能绑定一个角色(profile),并应当可以设置每个账号的最大启动器会话数量。
启动器会话 信息序列化后符合以下格式:
{
"id":" ID(启动器会话ID)",
"properties":[ // 属性(数组,每一元素为一个属性)
{ // 一项属性
"name":"属性的名称",
"value":"属性的值",
}
// ,...(可以有更多)
]
}
启动器会话 属性中目前已知的项目如下:
| 名称 | 值 |
|---|---|
| preferredLanguage | (可选) 用户的偏好语言,例如 en、zh_CN |
此项将存储数据库数据表中。
角色与 账号 为多对一关系。一个角色对应 Minecraft 中的一个实体玩家。角色具有以下属性:
UUID 和名称均为全局唯一,但名称可变。应避免使用名称作为标识。
若不考虑兼容性,角色的 UUID 一般为随机生成(Version 4)。
但 Minecraft 仅使用 UUID 作为角色标识符,不同 UUID 的角色即使名称相同也被认为是不同的。如果一个 Minecraft 服务器从其他登录系统(正版验证、离线验证或其他)迁移到本登录系统,并且角色的 UUID 发生了变化,则该角色的数据将丢失。为了避免这种情况,必须保证对于同一个角色,本系统生成的 UUID 与其在先前系统中的 UUID 是相同的。
若 Minecraft 服务器原先采用的是离线验证,则角色 UUID 是角色名称的一元函数。如果 Yggdrasil 服务端使用此方法生成角色 UUID,就可以实现与离线验证系统之间的双向兼容,即可以在不丢失角色数据的情况下,在离线验证系统和本登录系统之间切换。
从角色名称计算角色 UUID 的代码如下(Java):
UUID.nameUUIDFromBytes(("OfflinePlayer:" + characterName).getBytes(StandardCharsets.UTF_8))
角色信息序列化后符合以下格式:
{
"id":"角色 UUID(无符号)",
"name":"角色名称",
"properties":[ // 角色的属性(数组,每一元素为一个属性)(仅在特定情况下需要包含)
{ // 一项属性
"name":"属性的名称",
"value":"属性的值",
"signature":"属性值的数字签名(仅在特定情况下需要包含)"
}
// ,...(可以有更多)
]
}
角色属性(properties)及数字签名(signature)在无特殊说明的情况下不需要包含。
signature 是属性值的数字签名,使用 Base64 编码。签名算法为 SHA1withRSA,见 PKCS #1。关于签名密钥的详细介绍,见 签名密钥对。
角色属性中可以包含以下项目:
| 名称 | 值 |
|---|---|
| textures | (可选)Base64 编码的 JSON 字符串,包含了角色的材质信息,详见 §textures 材质信息属性。 |
| uploadableTextures | (可选)该角色可以上传的材质类型,为 authlib-injector 自行规定的属性,详见 §uploadableTextures 可上传的材质类型 |
以下为材质信息的格式,将这段 JSON 进行 Base64 编码后,即为 textures 角色属性的值。
{
"timestamp":"该属性值被生成时的时间戳(Java 时间戳格式,即自 1970-01-01 00:00:00 UTC 至今经过的毫秒数)",
"profileId":"角色 UUID(无符号)",
"profileName":"角色名称",
"textures":{ // 角色的材质
"材质类型(如 SKIN)":{ // 若角色不具有该项材质,则不必包含
"url":"材质的 URL",
"metadata":{ // 材质的元数据,若没有则不必包含
"名称":"值"
// ,...(可以有更多)
}
}
// ,...(可以有更多)
}
}
材质元数据中目前已知的项目有 model,其对应该角色的材质模型,取值为 default 或 slim。
注意: 这一角色属性是由 authlib-injector 文档规定的,Mojang 返回的角色属性是不包含这一项的。Mojang 仅允许用户上传皮肤,不允许上传披风。
考虑到并非所有验证服务器都允许用户上传皮肤和披风,因此 authlib-injector 规定了 uploadableTextures 角色属性,其表示角色可以上传的材质类型。
该属性的值是一个逗号分隔的列表,包含了可以上传的材质类型。材质类型目前有 skin 和 cape 两种。
例如,uploadableTextures 属性的值若为 skin,则表示可以为该角色上传皮肤,但不能上传披风;值若为 skin,cape,则既可以上传皮肤,又可以上传披风。
如果不存在 uploadableTextures 属性,则不能为该角色上传任何类型的材质。
关于材质上传接口的介绍,请参考 §材质上传。
Minecraft 将材质 hash 作为材质的标识。每当客户端下载一个材质后,便会将其缓存在本地,以后若需要相同 hash 的材质,则会直接使用缓存。 而这个 hash 并不是由客户端计算的。Yggdrasil 服务端应先计算好材质 hash,将其作为材质 URL 的文件名,即从 URL 最后一个 /(不包括)开始一直到结尾的这一段子串。 而客户端会直接将 URL 的文件名作为材质的 hash。
例如下面这个 URL,它所代表的材质的 hash 为 e051c27e803ba15de78a1d1e83491411dffb6d7fd2886da0a6c34a2161f7ca99:
https://yggdrasil.example.com/textures/e051c27e803ba15de78a1d1e83491411dffb6d7fd2886da0a6c34a2161f7ca99
安全警告:
- 材质 URL 响应头中的 Content-Type 必须为 image/png。若未指定,则存在 MIME Sniffing Attack 的风险。
材质 hash 的计算方法服务端可自行选择,建议使用 SHA-256 或者更加安全的 hash 算法。作为参考,Mojang 官方的方法是计算图像文件的 SHA-256。
备注:若两个材质 hash 值一致,则认为是相同材质。
安全警告:
- 若不对用户上传材质进行处理,则可能导致远程代码执行
- 在读取材质前,若不先检查图像大小,则可导致拒绝服务攻击 关于此安全缺陷的详细信息:未经检查的用户上传材质可能导致远程代码执行 #10
除了位图数据外,PNG 文件还可以存储其他数据。如果 Yggdrasil 服务端不对用户上传的材质进行检查,则攻击者可以在其中藏匿恶意代码,并通过 Yggdrasil 服务端分发到客户端。因此,Yggdrasil 服务端必须对用户上传的材质进行处理,除去其中任何与位图无关的数据。具体做法如下:
实现提示:在 Java 中可使用
ImageReader.getWidth()在不读入整个图像的情况下获取其尺寸。
此项存储于缓存之中,不存数据库。
令牌与 账号(启动器会话) 为多对一关系。令牌是一种登录凭证,具有时效性。令牌具有以下属性:
其中 accessToken 和 clientToken 为任意字符串(可以是无符号 UUID 或 JWT)。accessToken 由服务端随机生成,clientToken 由客户端提供。
介于 accessToken 的随机性,它可以被作为主键。而 clientToken 不具有唯一性。
绑定的角色可以为空。它代表了能使用该令牌进行游戏的角色。
一个用户可以同时有多个令牌,但服务端也应该对令牌数量加以限制。当令牌数量超出限制(如 10 个)时,则应先吊销最旧的令牌,之后再颁发新的令牌。
令牌有以下三种状态:
令牌的状态只能由有效变为无效,或是由有效变为暂时失效再变为无效,这个过程是不可逆的。 刷新操作仅颁发一个新的令牌,并不能使原令牌重新回到有效状态。
令牌应当有一个过期时限(如 15 天)。当自颁发起所经过的时间超过该时限时,令牌过期。
Mojang 对暂时失效状态的实现是这样的: 对启动器而言,若令牌处于暂时失效状态,则会刷新令牌,获得一个新的处于有效状态的令牌; 对 Yggdrasil 服务端而言,仅最后颁发的令牌才是有效的,先前颁发的其它令牌都处于暂时失效状态。
Mojang 之所以这么做,可能是为了防止用户多地同时登录(仅使最后一个 session 有效)。但事实上,即使服务端没有实现暂时失效状态,启动器的逻辑也是可以正常工作的。
当然,就算我们要实现暂时失效状态,也并不需要以 Mojang 的实现为范本。只需要启动器能够正确处理,任何实现都是可以的。下面给出一个不同于 Mojang 的实现的例子:
取一个短于令牌过期时限的时间段作为有效和暂时失效的分界点。若自颁发起经过的时间在该时限内,则令牌有效;若超过该时限,但仍在过期时限内,则令牌暂时失效。 这种做法实现了这样的功能:玩家如果经常进行登录操作,除了第一次登录就不需要输入密码了。而当他长时间未登录时则需要重新输入密码。
本文档和本目录的文档几乎全部抄自原文,目的是不用同时参考两个文档编写。