AuthAPI-docs

端点:/yggdrasil

本端点为 Yggdrasil-服务端技术规范 的项目化实现,凡是兼容此规范的应用均能兼容本规范。
因此本目录下所有url路径、请求格式和相应格式都是不变的,但是为了方便理解和避免歧义,将一些原来的概念更改了名字。但本质没有改变。

注意:本目录下所有端点均独立于其他端点,如和其他端点冲突,请以本目录文件为准。除非特别说明, /yggdrasil 下所有端点不设 Cookie 身份验证,不设人机验证。

本目录索引

目录

错误信息格式 (完全参考Yggdrasil-服务端技术规范)

这里的 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.

数据格式 (完全参考Yggdrasil-服务端技术规范)

模型

启动器会话

此项将存储于数据库数据表中。

一个系统中可以存在若干个启动器会话启动器会话具有以下属性:

其中 ID(启动器会话ID) 为一个无符号 UUID。ID(启动器会话ID)邮箱(启动器会话登录名) 不可变更,且需要保证唯一。

实际上,启动器会话应该绑定且只能绑定一个角色(profile),并应当可以设置每个账号的最大启动器会话数量。

启动器会话信息的序列化

启动器会话 信息序列化后符合以下格式:

{
  "id":" ID(启动器会话ID)",
  "properties":[ // 属性(数组,每一元素为一个属性)
    { // 一项属性
      "name":"属性的名称",
      "value":"属性的值",
    }
    // ,...(可以有更多)
  ]
}

启动器会话 属性中目前已知的项目如下:

名称
preferredLanguage (可选) 用户的偏好语言,例如 en、zh_CN

角色(Profile)

此项将存储数据库数据表中。

角色与 账号 为多对一关系。一个角色对应 Minecraft 中的一个实体玩家。角色具有以下属性:

UUID 和名称均为全局唯一,但名称可变。应避免使用名称作为标识。

角色 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 可上传的材质类型

textures 材质信息属性

以下为材质信息的格式,将这段 JSON 进行 Base64 编码后,即为 textures 角色属性的值。

{
    "timestamp":"该属性值被生成时的时间戳(Java 时间戳格式,即自 1970-01-01 00:00:00 UTC 至今经过的毫秒数)",
    "profileId":"角色 UUID(无符号)",
    "profileName":"角色名称",
    "textures":{ // 角色的材质
        "材质类型(如 SKIN)":{ // 若角色不具有该项材质,则不必包含
            "url":"材质的 URL",
            "metadata":{ // 材质的元数据,若没有则不必包含
                "名称":"值"
                // ,...(可以有更多)
            }
        }
        // ,...(可以有更多)
    }
}

材质元数据中目前已知的项目有 model,其对应该角色的材质模型,取值为 defaultslim

uploadableTextures 可上传的材质类型

注意: 这一角色属性是由 authlib-injector 文档规定的,Mojang 返回的角色属性是不包含这一项的。Mojang 仅允许用户上传皮肤,不允许上传披风。

考虑到并非所有验证服务器都允许用户上传皮肤和披风,因此 authlib-injector 规定了 uploadableTextures 角色属性,其表示角色可以上传的材质类型。

该属性的值是一个逗号分隔的列表,包含了可以上传的材质类型。材质类型目前有 skincape 两种。

例如,uploadableTextures 属性的值若为 skin,则表示可以为该角色上传皮肤,但不能上传披风;值若为 skin,cape,则既可以上传皮肤,又可以上传披风。

如果不存在 uploadableTextures 属性,则不能为该角色上传任何类型的材质。

关于材质上传接口的介绍,请参考 §材质上传

材质 URL 规范

Minecraft 将材质 hash 作为材质的标识。每当客户端下载一个材质后,便会将其缓存在本地,以后若需要相同 hash 的材质,则会直接使用缓存。 而这个 hash 并不是由客户端计算的。Yggdrasil 服务端应先计算好材质 hash,将其作为材质 URL 的文件名,即从 URL 最后一个 /(不包括)开始一直到结尾的这一段子串。 而客户端会直接将 URL 的文件名作为材质的 hash。

例如下面这个 URL,它所代表的材质的 hash 为 e051c27e803ba15de78a1d1e83491411dffb6d7fd2886da0a6c34a2161f7ca99:

https://yggdrasil.example.com/textures/e051c27e803ba15de78a1d1e83491411dffb6d7fd2886da0a6c34a2161f7ca99

安全警告:

材质 hash 的计算方法服务端可自行选择,建议使用 SHA-256 或者更加安全的 hash 算法。作为参考,Mojang 官方的方法是计算图像文件的 SHA-256。

备注:若两个材质 hash 值一致,则认为是相同材质。

用户上传材质的安全性

安全警告:

除了位图数据外,PNG 文件还可以存储其他数据。如果 Yggdrasil 服务端不对用户上传的材质进行检查,则攻击者可以在其中藏匿恶意代码,并通过 Yggdrasil 服务端分发到客户端。因此,Yggdrasil 服务端必须对用户上传的材质进行处理,除去其中任何与位图无关的数据。具体做法如下:

  1. 读取该 PNG 文件中图像的大小,如果过大则应拒绝。
    • 即使是非常小的 PNG 文件也可以存储一幅足以消耗计算机所有内存的图像(即 PNG Bomb),因此切不可在检查图像大小前就将其完整读入。
  2. 检查图像是否为合法的皮肤/披风材质。
    • 皮肤的宽高为 64x32 的整数倍或 64x64 的整数倍,披风的宽高为 64x32 的整数倍或 22x17 的整数倍。宽高为 22x17 整数倍的披风并非标准尺寸的披风,服务端需要用透明像素将其宽高补足至 64x32 的整数倍。
  3. 将图像文件重新保存,去除无关的元数据。

实现提示:在 Java 中可使用 ImageReader.getWidth() 在不读入整个图像的情况下获取其尺寸。

令牌(Token)

此项存储于缓存之中,不存数据库。

令牌与 账号(启动器会话) 为多对一关系。令牌是一种登录凭证,具有时效性。令牌具有以下属性:

其中 accessTokenclientToken 为任意字符串(可以是无符号 UUID 或 JWT)。accessToken 由服务端随机生成,clientToken 由客户端提供。

介于 accessToken 的随机性,它可以被作为主键。而 clientToken 不具有唯一性。

绑定的角色可以为空。它代表了能使用该令牌进行游戏的角色。

一个用户可以同时有多个令牌,但服务端也应该对令牌数量加以限制。当令牌数量超出限制(如 10 个)时,则应先吊销最旧的令牌,之后再颁发新的令牌。

令牌的状态

令牌有以下三种状态:

令牌的状态只能由有效变为无效,或是由有效变为暂时失效再变为无效,这个过程是不可逆的。 刷新操作仅颁发一个新的令牌,并不能使原令牌重新回到有效状态。

令牌应当有一个过期时限(如 15 天)。当自颁发起所经过的时间超过该时限时,令牌过期。

关于暂时失效状态

Mojang 对暂时失效状态的实现是这样的: 对启动器而言,若令牌处于暂时失效状态,则会刷新令牌,获得一个新的处于有效状态的令牌; 对 Yggdrasil 服务端而言,仅最后颁发的令牌才是有效的,先前颁发的其它令牌都处于暂时失效状态。

Mojang 之所以这么做,可能是为了防止用户多地同时登录(仅使最后一个 session 有效)。但事实上,即使服务端没有实现暂时失效状态,启动器的逻辑也是可以正常工作的。

当然,就算我们要实现暂时失效状态,也并不需要以 Mojang 的实现为范本。只需要启动器能够正确处理,任何实现都是可以的。下面给出一个不同于 Mojang 的实现的例子:

取一个短于令牌过期时限的时间段作为有效和暂时失效的分界点。若自颁发起经过的时间在该时限内,则令牌有效;若超过该时限,但仍在过期时限内,则令牌暂时失效。 这种做法实现了这样的功能:玩家如果经常进行登录操作,除了第一次登录就不需要输入密码了。而当他长时间未登录时则需要重新输入密码。

备注

本文档和本目录的文档几乎全部抄自原文,目的是不用同时参考两个文档编写。