copy wiki pages to docs/

wiki revision: f583a9f3fce652bfd198c1c80cbb15d794ea75fb
This commit is contained in:
Haowei Wen 2021-03-06 22:01:05 +08:00
parent a656770c30
commit 999ff99751
No known key found for this signature in database
GPG key ID: 5BC167F73EA558E4
10 changed files with 1346 additions and 0 deletions

45
docs/Home.md Normal file
View file

@ -0,0 +1,45 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
目录
- [简介](#%E7%AE%80%E4%BB%8B)
- [相关项目](#%E7%9B%B8%E5%85%B3%E9%A1%B9%E7%9B%AE)
- [推荐的公共验证服务器](#%E6%8E%A8%E8%8D%90%E7%9A%84%E5%85%AC%E5%85%B1%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8)
- [捐助](#%E6%8D%90%E5%8A%A9)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 简介
该项目的目标:
* 为修改 Minecraft ,使其使用自定义 Yggdrasil 服务提供工具
* 为自定义 Yggdrasil 服务端、使用自定义 Yggdrasil 服务的启动器提供技术规范
* 为玩家提供统一的非 Mojang 游戏外登录体验
* 玩家可以使用任意实现该规范的启动器,登录任意实现该规范的 Yggdrasil 服务
该项目会对所有 API 作出详细说明,并且还会定义一些不属于 Yggdrasil 的 API 。这样做是为了最简化指定 Yggdrasil 服务的流程:只需要填写 Yggdrasil 服务对应的 URL ,就可以使用它。
如果你是 Yggdrasil 服务端开发者或启动器开发者,或是对本项目感兴趣,请 Watch 本项目,以了解规范最新的发展
> 开发者交流 QQ 群926979364Telegram 群:[@authlib_injector](https://t.me/authlib_injector)。欢迎启动器或皮肤站开发者加入。普通用户请勿加群,可能会听不懂。
## 相关项目
* [yggdrasil-mock](https://github.com/yushijinhun/yggdrasil-mock)
* Yggdrasil 服务端规范的参考实现,以及 Yggdrasil API 的测试用例
* 基于此项目的 Yggdrasil 服务端演示站点:[auth-demo.yushi.moe](https://github.com/yushijinhun/yggdrasil-mock/wiki/演示站点)
* [BMCLAPI](https://bmclapidoc.bangbang93.com/#api-Mirrors-Mirrors_authlib_injector)
* BMCLAPI 为 authlib-injector 下载提供了一个镜像
* [Yggdrasil API for Blessing Skin](https://blessing.netlify.app/yggdrasil-api/)
* Blessing Skin 皮肤站的 Yggdrasil 插件
* [HMCL](https://github.com/huanghongxun/HMCL)
* HMCL v3.x 支持 authlib-injector
* [BakaXL](https://www.bakaxl.com/)
* BakaXL 3.0 支持 authlib-injector
* [LaunchHelper](https://github.com/Codex-in-somnio/LaunchHelper)
* 想在 Multicraft 面板服上使用 authlib-injector可以尝试此项目
## 推荐的公共验证服务器
* [LittleSkin](https://littlesk.in/)
* [Ely.by](https://ely.by/)
## 捐助
BMCLAPI 为 authlib-injector 提供了下载镜像站。如果您想要支持 authlib-injector 的开发,您可以[捐助 BMCLAPI](https://bmclapidoc.bangbang93.com/)。

View file

@ -0,0 +1,842 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
目录
=================
- [概述](#%E6%A6%82%E8%BF%B0)
- [基本约定](#%E5%9F%BA%E6%9C%AC%E7%BA%A6%E5%AE%9A)
- [字符编码](#%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81)
- [请求与响应格式](#%E8%AF%B7%E6%B1%82%E4%B8%8E%E5%93%8D%E5%BA%94%E6%A0%BC%E5%BC%8F)
- [错误信息格式](#%E9%94%99%E8%AF%AF%E4%BF%A1%E6%81%AF%E6%A0%BC%E5%BC%8F)
- [数据格式](#%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F)
- [模型](#%E6%A8%A1%E5%9E%8B)
- [用户](#%E7%94%A8%E6%88%B7)
- [用户信息的序列化](#%E7%94%A8%E6%88%B7%E4%BF%A1%E6%81%AF%E7%9A%84%E5%BA%8F%E5%88%97%E5%8C%96)
- [角色Profile](#%E8%A7%92%E8%89%B2profile)
- [角色 UUID 的生成](#%E8%A7%92%E8%89%B2-uuid-%E7%9A%84%E7%94%9F%E6%88%90)
- [兼容离线验证](#%E5%85%BC%E5%AE%B9%E7%A6%BB%E7%BA%BF%E9%AA%8C%E8%AF%81)
- [角色信息的序列化](#%E8%A7%92%E8%89%B2%E4%BF%A1%E6%81%AF%E7%9A%84%E5%BA%8F%E5%88%97%E5%8C%96)
- [`textures` 材质信息属性](#textures-%E6%9D%90%E8%B4%A8%E4%BF%A1%E6%81%AF%E5%B1%9E%E6%80%A7)
- [`uploadableTextures` 可上传的材质类型](#uploadabletextures-%E5%8F%AF%E4%B8%8A%E4%BC%A0%E7%9A%84%E6%9D%90%E8%B4%A8%E7%B1%BB%E5%9E%8B)
- [材质 URL 规范](#%E6%9D%90%E8%B4%A8-url-%E8%A7%84%E8%8C%83)
- [用户上传材质的安全性](#%E7%94%A8%E6%88%B7%E4%B8%8A%E4%BC%A0%E6%9D%90%E8%B4%A8%E7%9A%84%E5%AE%89%E5%85%A8%E6%80%A7)
- [令牌Token](#%E4%BB%A4%E7%89%8Ctoken)
- [令牌的状态](#%E4%BB%A4%E7%89%8C%E7%9A%84%E7%8A%B6%E6%80%81)
- [关于暂时失效状态](#%E5%85%B3%E4%BA%8E%E6%9A%82%E6%97%B6%E5%A4%B1%E6%95%88%E7%8A%B6%E6%80%81)
- [Yggdrasil API](#yggdrasil-api)
- [用户部分](#%E7%94%A8%E6%88%B7%E9%83%A8%E5%88%86)
- [登录](#%E7%99%BB%E5%BD%95)
- [使用角色名称登录](#%E4%BD%BF%E7%94%A8%E8%A7%92%E8%89%B2%E5%90%8D%E7%A7%B0%E7%99%BB%E5%BD%95)
- [刷新](#%E5%88%B7%E6%96%B0)
- [验证令牌](#%E9%AA%8C%E8%AF%81%E4%BB%A4%E7%89%8C)
- [吊销令牌](#%E5%90%8A%E9%94%80%E4%BB%A4%E7%89%8C)
- [登出](#%E7%99%BB%E5%87%BA)
- [会话部分](#%E4%BC%9A%E8%AF%9D%E9%83%A8%E5%88%86)
- [客户端进入服务器](#%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%BF%9B%E5%85%A5%E6%9C%8D%E5%8A%A1%E5%99%A8)
- [服务端验证客户端](#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E9%AA%8C%E8%AF%81%E5%AE%A2%E6%88%B7%E7%AB%AF)
- [角色部分](#%E8%A7%92%E8%89%B2%E9%83%A8%E5%88%86)
- [查询角色属性](#%E6%9F%A5%E8%AF%A2%E8%A7%92%E8%89%B2%E5%B1%9E%E6%80%A7)
- [按名称批量查询角色](#%E6%8C%89%E5%90%8D%E7%A7%B0%E6%89%B9%E9%87%8F%E6%9F%A5%E8%AF%A2%E8%A7%92%E8%89%B2)
- [材质上传](#%E6%9D%90%E8%B4%A8%E4%B8%8A%E4%BC%A0)
- [PUT 上传材质](#put-%E4%B8%8A%E4%BC%A0%E6%9D%90%E8%B4%A8)
- [DELETE 清除材质](#delete-%E6%B8%85%E9%99%A4%E6%9D%90%E8%B4%A8)
- [扩展 API](#%E6%89%A9%E5%B1%95-api)
- [API 元数据获取](#api-%E5%85%83%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96)
- [材质域名白名单](#%E6%9D%90%E8%B4%A8%E5%9F%9F%E5%90%8D%E7%99%BD%E5%90%8D%E5%8D%95)
- [`meta` 中的元数据](#meta-%E4%B8%AD%E7%9A%84%E5%85%83%E6%95%B0%E6%8D%AE)
- [服务端基本信息](#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%9F%BA%E6%9C%AC%E4%BF%A1%E6%81%AF)
- [服务器网址](#%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BD%91%E5%9D%80)
- [功能选项](#%E5%8A%9F%E8%83%BD%E9%80%89%E9%A1%B9)
- [响应示例](#%E5%93%8D%E5%BA%94%E7%A4%BA%E4%BE%8B)
- [API 地址指示ALI](#api-%E5%9C%B0%E5%9D%80%E6%8C%87%E7%A4%BAali)
- [参见](#%E5%8F%82%E8%A7%81)
- [参考实现](#%E5%8F%82%E8%80%83%E5%AE%9E%E7%8E%B0)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# 概述
本文旨在为实现 Yggdrasil 服务端提供非官方的技术规范。
本规范中所描述的服务端行为不一定与 Mojang 服务端的相同。
这客观上是因为 Mojang 的服务端是闭源的,我们只能推测其内部逻辑,而所作出的推测难免会与实际存在出入。
但事实上只要客户端能够正确理解并处理服务端的响应,那么其行为是否与 Mojang 服务端的相同,也就无关紧要了。
# 基本约定
## 字符编码
本文中字符编码一律使用 UTF-8。
## 请求与响应格式
若无特殊说明,请求与响应均为 JSON 格式(如果有 body`Content-Type` 均为 `application/json; charset=utf-8`
所有 API 都应该使用 HTTPS 协议。
## 错误信息格式
```javascript
{
"error":"错误的简要描述(机器可读)",
"errorMessage":"错误的详细信息(人类可读)",
"cause":"该错误的原因(可选)"
}
```
当遇到本文中已说明的异常情况时,返回的错误信息应符合对应的要求。
下表列举了常见异常情况下的错误信息。除特殊说明外,`cause` 一般不包含。
**非标准**指由于无法使 Mojang 的 Yggdrasil 服务器触发对应异常,而只能推测该种情况下的错误信息。
**未定义**指该项并没有明确要求。
|异常情况|HTTP状态码|Error|Error Message|
|--------|----------|-----|------------|
|一般 HTTP 异常(非业务异常,如 _Not Found_、_Method Not Allowed_|_未定义_|_该 HTTP 状态对应的 Reason Phrase于 [HTTP/1.1](https://tools.ietf.org/html/rfc2616#section-6.1.1) 中定义_|_未定义_|
|令牌无效|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.|
## 数据格式
我们约定以下数据格式
* **无符号 UUID**: 指去掉所有 `-` 字符后的 UUID 字符串
## 模型
### 用户
一个系统中可以存在若干个用户,用户具有以下属性:
* ID
* 邮箱
* 密码
其中 ID 为一个无符号 UUID。邮箱可以变更但需要保证唯一。
#### 用户信息的序列化
用户信息序列化后符合以下格式:
```javascript
{
"id":"用户的 ID",
"properties":[ // 用户的属性(数组,每一元素为一个属性)
{ // 一项属性
"name":"属性的名称",
"value":"属性的值",
}
// ,...(可以有更多)
]
}
```
用户属性中目前已知的项目如下:
|名称|值|
|--|--------|
|preferredLanguage|**(可选)**用户的偏好语言,例如 `en`、`zh_CN`|
### 角色Profile
> Mojang 当前不支持多角色,不保证多角色部分内容的正确性。
角色与账号为多对一关系。一个角色对应 Minecraft 中的一个实体玩家。角色具有以下属性:
* UUID
* 名称
* 材质模型,可选值有:`default`、`slim`
* **default**正常手臂宽度4px的皮肤
* **slim**细手臂3px的皮肤
* 材质
* 类型为映射
* key 可选值有SKIN、CAPE
* value 类型为 URL
UUID 和名称均为全局唯一,但名称可变。应避免使用名称作为标识。
#### 角色 UUID 的生成
若不考虑兼容性,角色的 UUID 一般为随机生成([Version 4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)))。
但 Minecraft 仅使用 UUID 作为角色标识符,不同 UUID 的角色即使名称相同也被认为是不同的。如果一个 Minecraft 服务器从其他登录系统(正版验证、离线验证或其他)迁移到本登录系统,并且角色的 UUID 发生了变化,则该角色的数据将丢失。为了避免这种情况,必须保证对于同一个角色,本系统生成的 UUID 与其在先前系统中的 UUID 是相同的。
##### 兼容离线验证
若 Minecraft 服务器原先采用的是离线验证,则角色 UUID 是角色名称的一元函数。如果 Yggdrasil 服务端使用此方法生成角色 UUID就可以实现与离线验证系统之间的双向兼容即可以在不丢失角色数据的情况下在离线验证系统和本登录系统之间切换。
从角色名称计算角色 UUID 的代码如下Java
```java
UUID.nameUUIDFromBytes(("OfflinePlayer:" + characterName).getBytes(StandardCharsets.UTF_8))
```
在其他语言中的实现:
* [PHP](https://gist.github.com/games647/2b6a00a8fc21fd3b88375f03c9e2e603)
#### 角色信息的序列化
角色信息序列化后符合以下格式:
```javascript
{
"id":"角色 UUID无符号",
"name":"角色名称",
"properties":[ // 角色的属性(数组,每一元素为一个属性)(仅在特定情况下需要包含)
{ // 一项属性
"name":"属性的名称",
"value":"属性的值",
"signature":"属性值的数字签名(仅在特定情况下需要包含)"
}
// ,...(可以有更多)
]
}
```
角色属性(`properties`)及数字签名(`signature`)在无特殊说明的情况下不需要包含。
`signature` 是属性值的数字签名,使用 Base64 编码。签名算法为 SHA1withRSA见 [PKCS #1](https://www.rfc-editor.org/rfc/rfc2437.txt)。关于签名密钥的详细介绍,见 [签名密钥对](签名密钥对)。
角色属性中可以包含以下项目:
|名称|值|
|----|--|
|textures|可选Base64 编码的 JSON 字符串,包含了角色的材质信息,详见 [§`textures` 材质信息属性](#textures-材质信息属性)。|
|uploadableTextures|(可选)该角色可以上传的材质类型,为 authlib-injector 自行规定的属性,详见 [§`uploadableTextures` 可上传的材质类型](#uploadableTextures-可上传的材质类型)|
#### `textures` 材质信息属性
以下为材质信息的格式,将这段 JSON 进行 Base64 编码后,即为 `textures` 角色属性的值。
```javascript
{
"timestamp":该属性值被生成时的时间戳Java 时间戳格式,即自 1970-01-01 00:00:00 UTC 至今经过的毫秒数),
"profileId":"角色 UUID无符号",
"profileName":"角色名称",
"textures":{ // 角色的材质
"材质类型(如 SKIN":{ // 若角色不具有该项材质,则不必包含
"url":"材质的 URL",
"metadata":{ // 材质的元数据,若没有则不必包含
"名称":"值"
// ,...(可以有更多)
}
}
// ,...(可以有更多)
}
}
```
材质元数据中目前已知的项目有 `model`,其对应该角色的材质模型,取值为 `default``slim`
#### `uploadableTextures` 可上传的材质类型
> **注意:** 这一角色属性是由 authlib-injector 文档规定的Mojang 返回的角色属性是不包含这一项的。Mojang 仅允许用户上传皮肤,不允许上传披风。
考虑到并非所有验证服务器都允许用户上传皮肤和披风,因此 authlib-injector 规定了 `uploadableTextures` 角色属性,其表示角色可以上传的材质类型。
该属性的值是一个逗号分隔的列表,包含了可以上传的材质类型。材质类型目前有 `skin``cape` 两种。
例如,`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
```
> 安全警告:
> * 材质 URL 响应头中的 `Content-Type` 必须为 `image/png`。若未指定,则存在 MIME Sniffing Attack 的风险。
由于 PNG 格式图像包含与显示无关的数据,因此即使是图像尺寸与内容完全相同的 PNG 文件,它们的 hash 值也可能不同。
为此,需要使用一个仅与图像内容有关的方法来计算材质的 hash。规定这个方法如下
1. 首先创建一个长度为 `(width * height * 4 + 8)` 字节的缓冲区,其中 `width``height` 为图像的长和宽
2. 填充该缓冲区
1. `0~3` 字节为 `width`,以大端序存储
2. `4~7` 字节为 `height`,以大端序存储
3. 对于每一个像素,设其坐标为 `(x, y)`,其首地址 `offset``((y + x * height) * 4 + 8)`
1. 第 `(offset + 0)`、`(offset + 1)`、`(offset + 2)`、`(offset + 3)` 个字节分别为该像素的 Alpha、Red、Green、Blue 分量
2. 若 Alpha 分量为 `0x00`(透明),则 RGB 分量皆作为 `0x00` 处理
3. 计算以上缓冲区内数据的 `SHA-256`,作为材质的 hash
> 目前无法确定 Mojang 所使用的 hash 算法其输出长度61 个 hex 字符)不属于任何已知 hash 算法。
> 如果使用 SHA-256 作为 hash 算法,由于输出长度不同,不会与 Mojang 的 hash 算法发生冲突,因此是可行的。
<details>
<summary>Java 实现示例</summary>
```java
public static String textureHash(BufferedImage img) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
int width = img.getWidth();
int height = img.getHeight();
byte[] buf = new byte[4096];
putInt(buf, 0, width); // 0~3: width(big-endian)
putInt(buf, 4, height); // 4~7: height(big-endian)
int pos = 8;
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
// pos+0: alpha
// pos+1: red
// pos+2: green
// pos+3: blue
putInt(buf, pos, img.getRGB(x, y));
if (buf[pos + 0] == 0) {
// the pixel is transparent
buf[pos + 1] = buf[pos + 2] = buf[pos + 3] = 0;
}
pos += 4;
if (pos == buf.length) {
// buffer is full
pos = 0;
digest.update(buf, 0, buf.length);
}
}
}
if (pos > 0) {
// flush
digest.update(buf, 0, pos);
}
byte[] sha256 = digest.digest();
return String.format("%0" + (sha256.length << 1) + "x", new BigInteger(1, sha256)); // to hex
}
// put an int into the array in big-endian
private static void putInt(byte[] array, int offset, int x) {
array[offset + 0] = (byte) (x >> 24 & 0xff);
array[offset + 1] = (byte) (x >> 16 & 0xff);
array[offset + 2] = (byte) (x >> 8 & 0xff);
array[offset + 3] = (byte) (x >> 0 & 0xff);
}
```
</details>
<details>
<summary>JavaScript 实现示例</summary>
> 本实现使用了 [pngjs-nozlib](https://www.npmjs.com/package/pngjs-nozlib)。
```javascript
let crypto = require("crypto");
let PNG = require("pngjs-nozlib").PNG;
let fs = require("fs");
function computeTextureHash(image) {
const bufSize = 8192;
let hash = crypto.createHash("sha256");
let buf = Buffer.allocUnsafe(bufSize);
let width = image.width;
let height = image.height;
buf.writeUInt32BE(width, 0);
buf.writeUInt32BE(height, 4);
let pos = 8;
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
let imgidx = (width * y + x) << 2;
let alpha = image.data[imgidx + 3];
buf.writeUInt8(alpha, pos + 0);
if (alpha === 0) {
buf.writeUInt8(0, pos + 1);
buf.writeUInt8(0, pos + 2);
buf.writeUInt8(0, pos + 3);
} else {
buf.writeUInt8(image.data[imgidx + 0], pos + 1);
buf.writeUInt8(image.data[imgidx + 1], pos + 2);
buf.writeUInt8(image.data[imgidx + 2], pos + 3);
}
pos += 4;
if (pos === bufSize) {
pos = 0;
hash.update(buf);
}
}
}
if (pos > 0) {
hash.update(buf.slice(0, pos));
}
return hash.digest("hex");
}
console.info(computeTextureHash(PNG.sync.read(fs.readFileSync("texture-hash-test.png"))));
```
</details>
<details>
<summary>测试样例</summary>
> 样例输入:[texture-hash-test.png](https://raw.githubusercontent.com/wiki/yushijinhun/authlib-injector/texture-hash-test.png)
>
> 样例输出:`47a4c518f80f94ad8737713e0325a98e1f2647f962b9a646f58cd0bbd5afe683`
>
> 使用上述方法将图片读入到缓冲区,缓冲区中内容如下:
> ```
> 00 00 00 02 // 宽度2
> 00 00 00 03 // 高度3
> ff ff 00 00 // 像素 (0,0):红
> ff 00 00 ff // 像素 (0,1):蓝
> ff ff 00 ff // 像素 (0,2):紫
> ff 00 ff 00 // 像素 (1,0):绿
> 00 00 00 00 // 像素 (1,1):透明
> ff ff ff 00 // 像素 (1,2):黄
> ```
</details>
建议所有 Yggdrasil 服务端实现都应将上述算法作为材质 hash 的计算方法。
这样可以确保即使相同的材质来自不同的 Yggdrasil 服务端,它们的 hash 也是相同的,进而避免客户端不必要的重复下载和存储。
#### 用户上传材质的安全性
> 安全警告:
> * 若不对用户上传材质进行处理,则**可能导致远程代码执行**
> * 在读取材质前,若不先检查图像大小,则**可导致拒绝服务攻击**
>
> 关于此安全缺陷的详细信息:[未经检查的用户上传材质可能导致远程代码执行 #10](https://github.com/yushijinhun/authlib-injector/issues/10)
除了位图数据外PNG 文件还可以存储其他数据。如果 Yggdrasil 服务端不对用户上传的材质进行检查,则攻击者可以在其中藏匿恶意代码,并通过 Yggdrasil 服务端分发到客户端。因此Yggdrasil 服务端**必须**对用户上传的材质进行处理,除去其中任何与位图无关的数据。具体做法如下:
1. 读取该 PNG 文件中图像的大小,如果过大则应拒绝。
* 即使是非常小的 PNG 文件也可以存储一幅足以消耗计算机所有内存的图像(即 PNG Bomb因此切不可在检查图像大小前就将其完整读入。
2. 检查图像是否为合法的皮肤/披风材质。
* 皮肤的宽高为 64x32 的整数倍或 64x64 的整数倍,披风的宽高为 64x32 的整数倍或 22x17 的整数倍。宽高为 22x17 整数倍的披风并非标准尺寸的披风,服务端需要用透明像素将其宽高补足至 64x32 的整数倍。
3. 计算该材质的 hash[计算方法见上](#材质-url-规范)),并将其位图数据保存。
* 切不可直接保存用户上传的 PNG 文件。
> 实现提示:在 Java 中可使用 [`ImageReader.getWidth()`](https://docs.oracle.com/javase/10/docs/api/javax/imageio/ImageReader.html#getWidth(int)) 在不读入整个图像的情况下获取其尺寸。
### 令牌Token
令牌与账号为多对一关系。令牌是一种登录凭证,具有时效性。令牌具有以下属性:
* accessToken
* clientToken
* 绑定的角色
* 颁发时间
其中 `accessToken``clientToken` 为任意字符串(可以是无符号 UUID 或 JWT。`accessToken` 由服务端随机生成,`clientToken` 由客户端提供。
介于 `accessToken` 的随机性,它可以被作为主键。而 `clientToken` 不具有唯一性。
绑定的角色可以为空。它代表了能使用该令牌进行游戏的角色。
一个用户可以同时有多个令牌,但服务端也应该对令牌数量加以限制。当令牌数量超出限制(如 10 个)时,则应先吊销最旧的令牌,之后再颁发新的令牌。
#### 令牌的状态
令牌有以下三种状态:
* **有效**
* 处于该状态的令牌可以进行各项操作,如[进服验证](#会话部分)、[刷新](#刷新)。
* 新颁发的令牌(即通过[登录](#登录)、[刷新](#刷新)颁发的令牌)处于该状态。
* **暂时失效**
* 处于该状态的令牌除了进行[刷新操作](#刷新)外,无权进行任何操作。
* 当令牌绑定的角色改名后,令牌应被标记为**暂时失效**状态。
* 这是为了让启动器刷新令牌,从而获取到新的角色名称。(见 [#40](https://github.com/yushijinhun/authlib-injector/issues/40)
* _该状态并不是必须要实现的详细介绍见下。_
* **无效**
* 处于该状态的令牌无权进行任何操作。
* 令牌被吊销后处于该状态。这里的吊销包括[显式吊销](#吊销令牌)、[登出](#登出)、[刷新](#刷新)后吊销原令牌、令牌过期。
令牌的状态只能由有效变为无效,或是由有效变为暂时失效再变为无效,这个过程是不可逆的。
刷新操作仅颁发一个新的令牌,并不能使原令牌重新回到有效状态。
令牌应当有一个过期时限(如 15 天)。当自颁发起所经过的时间超过该时限时,令牌过期。
##### 关于暂时失效状态
Mojang 对暂时失效状态的实现是这样的:
对启动器而言,若令牌处于暂时失效状态,则会刷新令牌,获得一个新的处于有效状态的令牌;
对 Yggdrasil 服务端而言,仅最后颁发的令牌才是有效的,先前颁发的其它令牌都处于暂时失效状态。
Mojang 之所以这么做,可能是为了防止用户多地同时登录(仅使最后一个 session 有效)。但事实上,即使服务端没有实现暂时失效状态,启动器的逻辑也是可以正常工作的。
当然,就算我们要实现暂时失效状态,也并不需要以 Mojang 的实现为范本。只需要启动器能够正确处理,任何实现都是可以的。下面给出一个不同于 Mojang 的实现的例子:
> 取一个短于令牌过期时限的时间段作为有效和暂时失效的分界点。若自颁发起经过的时间在该时限内,则令牌有效;若超过该时限,但仍在过期时限内,则令牌暂时失效。
>
> 这种做法实现了这样的功能:玩家如果经常进行登录操作,除了第一次登录就不需要输入密码了。而当他长时间未登录时则需要重新输入密码。
# Yggdrasil API
## 用户部分
### 登录
`POST /authserver/authenticate`
使用密码进行身份验证,并分配一个新的令牌。
请求格式:
```javascript
{
"username":"邮箱(或其他凭证,详见 §使用角色名称登录)",
"password":"密码",
"clientToken":"由客户端指定的令牌的 clientToken可选",
"requestUser":true/false, // 是否在响应中包含用户信息,默认 false
"agent":{
"name":"Minecraft",
"version":1
}
}
```
若请求中未包含 `clientToken`,服务端应该随机生成一个无符号 UUID 作为 `clientToken`。但需要注意 `clientToken` 可以为任何字符串,即请求中提供任何 `clientToken` 都是可以接受的,不一定要为无符号 UUID。
对于令牌要绑定的角色:若用户没有任何角色,则为空;若用户仅有一个角色,那么通常绑定到该角色;若用户有多个角色,通常为空,以便客户端进行选择。也就是说如果绑定的角色为空,则需要客户端进行角色选择。
响应格式:
```javascript
{
"accessToken":"令牌的 accessToken",
"clientToken":"令牌的 clientToken",
"availableProfiles":[ // 用户可用角色列表
// ,... 每一项为一个角色(格式见 §角色信息的序列化)
],
"selectedProfile":{
// ... 绑定的角色,若为空,则不需要包含(格式见 §角色信息的序列化)
},
"user":{
// ... 用户信息(仅当请求中 requestUser 为 true 时包含,格式见 §用户信息的序列化)
}
}
```
**安全提示:** 该 API 可以被用于密码暴力破解,应受到速率限制。限制应针对用户,而不是客户端 IP。
#### 使用角色名称登录
除使用邮箱登录外,验证服务器还可以允许用户使用角色名称登录。要实现这一点,验证服务器需要进行以下工作:
* 将 API 元数据中的 `feature.non_email_login` 字段设置为 true。见 [API 元数据获取§功能选项](#功能选项)
* 接受在[登录接口](#登录)中使用角色名称作为 `username` 参数。
当用户使用角色名称登录时,验证服务器应**自动将令牌绑定到相应角色**,即上文响应中的 `selectedProfile` 应为用户登录时所用的角色。
这种情况下,如果用户拥有多个角色,那么他可以省去选择角色的操作。考虑到某些程序不支持多角色(例如 Geyser还可以通过上述方法绕过角色选择。
### 刷新
`POST /authserver/refresh`
吊销原令牌,并颁发一个新的令牌。
请求格式:
```javascript
{
"accessToken":"令牌的 accessToken",
"clientToken":"令牌的 clientToken可选",
"requestUser":true/false, // 是否在响应中包含用户信息,默认 false
"selectedProfile":{
// ... 要选择的角色(可选,格式见 §角色信息的序列化)
}
}
```
当指定 `clientToken` 时,服务端应检查 `accessToken``clientToken` 是否有效,否则只需要检查 `accessToken`
颁发的新令牌的 `clientToken` 应与原令牌的相同。
如果请求中包含 `selectedProfile`,那么这就是一个选择角色的操作。此操作要求原令牌所绑定的角色为空,而新令牌则将绑定到 `selectedProfile` 所指定的角色上。如果不包含 `selectedProfile`,那么新令牌所绑定的角色和原令牌相同。
刷新操作在令牌暂时失效时依然可以执行。若请求失败,原令牌依然有效。
响应格式:
```javascript
{
"accessToken":"新令牌的 accessToken",
"clientToken":"新令牌的 clientToken",
"selectedProfile":{
// ... 新令牌绑定的角色,若为空,则不需要包含(格式见 §角色信息的序列化)
},
"user":{
// ... 用户信息(仅当请求中 requestUser 为 true 时包含,格式见 §用户信息的序列化)
}
}
```
### 验证令牌
`POST /authserver/validate`
检验令牌是否有效。
请求格式:
```javascript
{
"accessToken":"令牌的 accessToken",
"clientToken":"令牌的 clientToken可选"
}
```
当指定 `clientToken` 时,服务端应检查 `accessToken``clientToken` 是否有效,否则只需要检查 `accessToken`
若令牌有效,服务端应返回 HTTP 状态 `204 No Content`,否则作为令牌无效的异常情况处理。
### 吊销令牌
`POST /authserver/invalidate`
吊销给定令牌。
请求格式:
```javascript
{
"accessToken":"令牌的 accessToken",
"clientToken":"令牌的 clientToken可选"
}
```
服务端只需要检查 `accessToken`,即无论 `clientToken` 为何值都不会造成影响。
无论操作是否成功,服务端应返回 HTTP 状态 `204 No Content`
### 登出
`POST /authserver/signout`
吊销用户的所有令牌。
请求格式:
```javascript
{
"username":"邮箱",
"password":"密码"
}
```
若操作成功,服务端应返回 HTTP 状态 `204 No Content`
**安全提示:** 该 API 也可用于判断密码的正确性,因此应受到和登录 API 一样的速率限制。
## 会话部分
![Minecraft 玩家进服原理](https://raw.githubusercontent.com/wiki/yushijinhun/authlib-injector/mc入服原理.svg?sanitize=true)
> 上图使用 ProcessOn 绘制,导出为 SVG。[原始图像](https://www.processon.com/view/link/5a7fbbbae4b0812a0f102187)
该部分用于角色进入服务器时的验证。主要流程如下:
1. **Minecraft 服务端**和 **Minecraft 客户端**共同生成一段字符串(`serverId`),其可以被认为是随机的
2. **Minecraft 客户端**将 `serverId` 及令牌发送给 **Yggdrasil 服务端**(要求令牌有效)
3. **Minecraft 服务端**请求 **Yggdrasil 服务端**检查客户端会话的有效性,即客户端是否成功进行第 2 步
### 客户端进入服务器
`POST /sessionserver/session/minecraft/join`
记录服务端发送给客户端的 `serverId`,以备服务端检查。
请求格式:
```javascript
{
"accessToken":"令牌的 accessToken",
"selectedProfile":"该令牌绑定的角色的 UUID无符号",
"serverId":"服务端发送给客户端的 serverId"
}
```
仅当 `accessToken` 有效,且 `selectedProfile` 与令牌所绑定的角色一致时,操作才成功。
服务端应记录以下信息:
* serverId
* accessToken
* 发送该请求的客户端 IP
实现时请注意:以上信息应记录在内存数据库中(如 Redis且应该设置过期时间如 30 秒)。
介于 `serverId` 的随机性,可以将其作为主键。
若操作成功,服务端应返回 HTTP 状态 `204 No Content`
### 服务端验证客户端
`GET /sessionserver/session/minecraft/hasJoined?username={username}&serverId={serverId}&ip={ip}`
检查客户端会话的有效性,即数据库中是否存在该 `serverId` 的记录,且信息正确。
请求参数:
|参数|值|
|----|--|
|username|角色的名称|
|serverId|服务端发送给客户端的 serverId|
|ip _可选_|Minecraft 服务端获取到的客户端 IP仅当 [`prevent-proxy-connections`](https://minecraft.gamepedia.com/Server.properties#prevent-proxy-connections) 选项开启时包含|
`username` 需要与 `serverId` 所对应令牌所绑定的角色的名称相同。
响应格式:
```javascript
{
// ... 令牌所绑定角色的完整信息(包含角色属性及数字签名,格式见 §角色信息的序列化)
}
```
若操作失败,服务端应返回 HTTP 状态 `204 No Content`
## 角色部分
该部分用于角色信息的查询。
### 查询角色属性
`GET /sessionserver/session/minecraft/profile/{uuid}?unsigned={unsigned}`
查询指定角色的完整信息(包含角色属性)。
请求参数:
|参数|值|
|----|--|
|uuid|角色的 UUID无符号|
|unsigned _可选_|`true` 或 `false`。是否在响应中**不包含**数字签名,默认为 `true`|
响应格式:
```javascript
{
// ... 角色信息(包含角色属性。若 unsigned 为 false还需要包含数字签名。格式见 §角色信息的序列化)
}
```
若角色不存在,服务端应返回 HTTP 状态 `204 No Content`
### 按名称批量查询角色
`POST /api/profiles/minecraft`
批量查询角色名称所对应的角色。
请求格式:
```javascript
[
"角色名称"
// ,... 还可以有更多
]
```
服务端查询各个角色名称所对应的角色信息,并将其包含在响应中。不存在的角色不需要包含。响应中角色信息的先后次序无要求。
响应格式:
```javascript
[
{
// 角色信息(注意:不包含角色属性。格式见 §角色信息的序列化)
}
// ,...(可以有更多)
]
```
**安全提示:** 为防止 CC 攻击,需要为单次查询的角色数目设置最大值,该值至少为 2。
## 材质上传
```
PUT /api/user/profile/{uuid}/{textureType}
DELETE /api/user/profile/{uuid}/{textureType}
```
设置或清除指定角色的材质。
> 并非所有角色都可以上传皮肤和披风。要获取当前角色能够上传的材质类型,参见 [§`uploadableTextures` 可上传的材质类型](#uploadableTextures-可上传的材质类型)。
请求参数:
|参数|值|
|----|--|
|uuid|角色的 UUID无符号|
|textureType|材质类型,可以为 `skin`(皮肤)或 `cape`(披风)|
请求需要带上 HTTP 头部 `Authorization: Bearer {accessToken}` 进行认证。若未包含 Authorization 头或 accessToken 无效,则返回 `401 Unauthorized`
如果操作成功,则返回 `204 No Content`
下面分别介绍 PUT 和 DELETE 这两个 HTTP 方法的用法:
### PUT 上传材质
请求的 `Content-Type``multipart/form-data`,请求载荷由以下部分组成:
|名称name|内容|
|-----------|----|
|model |**(仅用于皮肤)** 皮肤的材质模型,可以为 `slim`(细胳膊皮肤)或空字符串(普通皮肤)。|
|file |材质图像,`Content-Type` 须为 `image/png`<br>建议客户端设置 `Content-Disposition` 中的 `filename` 参数为材质图像的文件名,这可以被验证服务器用作材质的备注。|
如果操作成功,则返回 `204 No Content`
### DELETE 清除材质
清除材质后,该类型的材质将恢复为默认。
# 扩展 API
以下 API 是为了方便 authlib-injector 进行自动配置而设计的。
## API 元数据获取
`GET /`
响应格式:
```javascript
{
"meta":{
// 服务端的元数据,内容任意
},
"skinDomains":[ // 材质域名白名单
"域名匹配规则 1"
// ,...
],
"signaturePublickey":"用于验证数字签名的公钥"
}
```
`signaturePublickey` 是 PEM 格式的公钥,用于验证角色属性的数字签名。其以 `-----BEGIN PUBLIC KEY-----` 开头,以 `-----END PUBLIC KEY-----` 结尾,中间允许出现换行符,但不允许出现其他空白字符(亦允许文末出现换行符)。
### 材质域名白名单
Minecraft 仅会从白名单中的域名下载材质。如果材质 URL 的域名不在白名单中,则会出现 `Textures payload has been tampered with (non-whitelisted domain)` 错误。采用此机制的原因见 [MC-78491](https://bugs.mojang.com/browse/MC-78491)。
材质白名单默认包含 `.minecraft.net`、`.mojang.com` 两项规则,你可以设置 `skinDomains` 属性以添加额外的白名单规则。规则格式如下:
* 如果规则以 `.`dot开头则匹配以这一规则结尾的域名。
* 例如 `.example.com` 匹配 `a.example.com`、`b.a.example.com`**不匹配** `example.com`
* 如果规则**不以** `.`dot开头则匹配的域名须与规则**完全相同**。
* 例如 `example.com` 匹配 `example.com`**不匹配** `a.example.com`、`eexample.com`。
### `meta` 中的元数据
`meta` 中的内容没有强制要求,以下字段均为可选。
#### 服务端基本信息
|Key|Value|
|---|-----|
|serverName|服务器名称|
|implementationName|服务端实现的名称|
|implementationVersion|服务端实现的版本|
#### 服务器网址
如果您需要在启动器中展示验证服务器首页地址、注册页面地址等信息,您可以在 `meta` 中添加一个 `links` 字段。
`links` 字段的类型是对象,其中可以包含:
|Key|Value|
|---|-----|
|homepage|验证服务器首页地址|
|register|注册页面地址|
#### 功能选项
> 以下带有 **_(advanced)_** 标注的字段为高级选项,通常情况下**不需要**设置。
|Key|Value|
|---|-----|
|feature.non\_email\_login|布尔值,指示验证服务器是否支持使用邮箱之外的凭证登录(如角色名登录),默认为 false。<br>详情见 [§使用角色名称登录](#使用角色名称登录)。|
|feature.legacy\_skin\_api|_(advanced)_ 布尔值,指示验证服务器是否支持旧式皮肤 API`GET /skins/MinecraftSkins/{username}.png`<br>当未指定或值为 false 时authlib-injector 会使用内建的 HTTP 服务器在本地处理对该 API 的请求;若值为 true请求将由验证服务器处理。<br>详情见 [README § 参数] 中的 `-Dauthlibinjector.legacySkinPolyfill` 选项。|
|feature.no\_mojang\_namespace|_(advanced)_ 布尔值,是否禁用 authlib-injector 的 Mojang 命名空间(@mojang 后缀)功能,默认为 false。<br>详情见 [README § 参数] 中的 `-Dauthlibinjector.mojangNamespace` 选项。|
[README § 参数]: https://github.com/yushijinhun/authlib-injector#参数
### 响应示例
```javascript
{
"meta": {
"implementationName": "yggdrasil-mock-server",
"implementationVersion": "0.0.1",
"serverName": "yushijinhun's Example Authentication Server",
"links": {
"homepage": "https://skin.example.com/",
"register": "https://skin.example.com/register"
},
"feature.non_email_login": true
},
"skinDomains": [
"example.com",
".example.com"
],
"signaturePublickey": "-----BEGIN PUBLIC KEY-----\nMIICIj...(省略)...EAAQ==\n-----END PUBLIC KEY-----\n"
}
```
# API 地址指示ALI
API 地址指示API Location Indication简称 ALI是一个 HTTP 响应头字段 `X-Authlib-Injector-API-Location`起到服务发现的作用。ALI 的值为相对 URL 或绝对 URL它指向与当前页面相关联的 Yggdrasil API。
使用 ALI 后,用户只需输入一个与 Yggdrasil API 相关联的地址即可,不必输入真正的 API 地址。例如,`https://skin.example.com/api/yggdrasil/` 可以被简化为 `skin.example.com`。支持 ALI 的启动器会请求 `(https://)skin.example.com`,识别响应中的 ALI 头字段,并根据它找到真正的 API 地址。
皮肤站可以在首页,或在全站启用 ALI。启用 ALI 的方法为在 HTTP 响应中添加 `X-Authlib-Injector-API-Location` 头字段,例如:
```
X-Authlib-Injector-API-Location: /api/yggdrasil/ # 使用相对 URL
X-Authlib-Injector-API-Location: https://skin.example.com/api/yggdrasil/ # 亦可使用绝对 URL支持跨域
```
当一个页面的 ALI 指向其本身时,这个 ALI 会被忽略。
# 参见
* [Authentication - wiki.vg](http://wiki.vg/Authentication)
* [Mojang API - wiki.vg](http://wiki.vg/Mojang_API)
* [Protocol - wiki.vg](http://wiki.vg/Protocol)
# 参考实现
[yggdrasil-mock](https://github.com/yushijinhun/yggdrasil-mock) 为本规范的参考实现。

3
docs/_Footer.md Normal file
View file

@ -0,0 +1,3 @@
[![Creative Commons License](https://i.creativecommons.org/l/by-sa/4.0/88x31.png)](https://creativecommons.org/licenses/by-sa/4.0/)
<br>
This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/).

1
docs/mc入服原理.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/texture-hash-test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>DnD 方式添加 Yggdrasil 服务端演示</title>
</head>
<body>
<p>
拖动右侧文本到启动器:
<span id="dndLabel" draggable="true" ondragstart="dndLabel_dragstart(event);">example.yggdrasil.yushi.moe</span>
</p>
<script>
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";
}
</script>
</body>
</html>

View file

@ -0,0 +1,239 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
目录
=================
- [概述](#%E6%A6%82%E8%BF%B0)
- [验证服务器](#%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8)
- [验证服务器的设置](#%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E8%AE%BE%E7%BD%AE)
- [在配置文件中指定](#%E5%9C%A8%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E4%B8%AD%E6%8C%87%E5%AE%9A)
- [在启动器中输入地址](#%E5%9C%A8%E5%90%AF%E5%8A%A8%E5%99%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E5%9C%B0%E5%9D%80)
- [处理 API 地址指示ALI](#%E5%A4%84%E7%90%86-api-%E5%9C%B0%E5%9D%80%E6%8C%87%E7%A4%BAali)
- [通过拖拽设置](#%E9%80%9A%E8%BF%87%E6%8B%96%E6%8B%BD%E8%AE%BE%E7%BD%AE)
- [拖动数据](#%E6%8B%96%E5%8A%A8%E6%95%B0%E6%8D%AE)
- [HTML 示例](#html-%E7%A4%BA%E4%BE%8B)
- [验证服务器信息的呈现](#%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%BF%A1%E6%81%AF%E7%9A%84%E5%91%88%E7%8E%B0)
- [服务器名称的显示](#%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%90%8D%E7%A7%B0%E7%9A%84%E6%98%BE%E7%A4%BA)
- [对非 HTTPS 验证服务器的警告](#%E5%AF%B9%E9%9D%9E-https-%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E8%AD%A6%E5%91%8A)
- [账户](#%E8%B4%A6%E6%88%B7)
- [账户信息的存储](#%E8%B4%A6%E6%88%B7%E4%BF%A1%E6%81%AF%E7%9A%84%E5%AD%98%E5%82%A8)
- [账户的添加](#%E8%B4%A6%E6%88%B7%E7%9A%84%E6%B7%BB%E5%8A%A0)
- [凭证有效性的确认](#%E5%87%AD%E8%AF%81%E6%9C%89%E6%95%88%E6%80%A7%E7%9A%84%E7%A1%AE%E8%AE%A4)
- [账户信息的显示](#%E8%B4%A6%E6%88%B7%E4%BF%A1%E6%81%AF%E7%9A%84%E6%98%BE%E7%A4%BA)
- [启动游戏](#%E5%90%AF%E5%8A%A8%E6%B8%B8%E6%88%8F)
- [下载 authlib-injector](#%E4%B8%8B%E8%BD%BD-authlib-injector)
- [配置预获取](#%E9%85%8D%E7%BD%AE%E9%A2%84%E8%8E%B7%E5%8F%96)
- [添加启动参数](#%E6%B7%BB%E5%8A%A0%E5%90%AF%E5%8A%A8%E5%8F%82%E6%95%B0)
- [配置 authlib-injector](#%E9%85%8D%E7%BD%AE-authlib-injector)
- [替换参数模板](#%E6%9B%BF%E6%8D%A2%E5%8F%82%E6%95%B0%E6%A8%A1%E6%9D%BF)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# 概述
本文旨在为启动器实现 authlib-injector 规范提供技术指导。由于该功能需要调用 Yggdrasil API因此建议您在阅读本文前先阅读 [Yggdrasil 服务端技术规范](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)](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)来设置验证服务器。
DnD 源可以是浏览器或者其他应用程序,而 DnD 目标则是启动器。DnD 源需要展示一段文字、图片或其他内容,示意用户将此内容拖入启动器,以添加验证服务端。在此过程中,验证服务器信息从 DnD 源传输到启动器。在完成 DnD 动作后,启动器向用户确认是否要添加此验证服务器。
#### 拖动数据
拖动数据的 MIME 类型为 `text/plain`,内容为一段 URI其格式如下
```
authlib-injector:yggdrasil-server:{验证服务端的 API 地址}
```
其中的 API 地址是 URI 的一个组成,应当被[编码](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent)。
拖动效果为复制(`copy`)。
#### HTML 示例
> [演示页面](https://rawgit.com/wiki/yushijinhun/authlib-injector/yggdrasil-server-dnd-example.html)
在需要拖动的 DOM 节点上添加 `draggable="true"`,并处理 `dragstart` 事件:
```html
<span id="dndLabel" draggable="true" ondragstart="dndLabel_dragstart(event);">example.yggdrasil.yushi.moe</span>
```
```javascript
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 请求,启动器可以获取到验证服务器的元数据([响应格式](Yggdrasil-服务端技术规范#api-元数据获取)),例如服务器名称。启动器可以利用这些元数据提升用户体验。
### 服务器名称的显示
验证服务器在 `meta` 中的 `serverName` 里指定了验证服务器的名称。当启动器需要向用户显示一个验证服务器时,便可以使用该名称。
需要注意的是,验证服务器名称可能会出现冲突,因此启动器应当提供查看验证服务器 API 地址的方法。例如,当鼠标悬浮在验证服务器名称上时,启动器在 Tooltip 中显示其 API 地址。
## 对非 HTTPS 验证服务器的警告
当用户尝试设置一个使用明文 HTTP 协议的验证服务器时,启动器应向用户显示醒目警告,告知用户这可能将其信息安全置于危险之中,用户的密码将会被明文传输。
# 账户
一个账户对应了游戏中的一个玩家,用户可以在启动时选择一个账户进行游戏。
> **账户、用户和角色的关系**:启动器中账户的概念,与验证服务器中用户的概念**并不相同**。与启动器中的账户相对应的是验证服务器中的角色Profile。验证服务器中的用户则是一个或多个角色的所有者其在启动器中并无对应实体。
## 账户信息的存储
启动器通过以下三个不可变的属性来确定一个账户:
* 账户所属验证服务器
* 账户的标识(如邮箱)
* 通常账户标识即为邮箱。但若验证服务器返回的元数据中 `feature.non_email_login` 字段为 true则表明验证服务器支持使用邮箱以外的凭证登录即账户标识可以不是邮箱。此时启动器**不应当**期望用户输入的账户一定是邮箱,同时应注意措辞(如使用「账户」一词代替「邮箱」一词),以免造成迷惑。(详见 [Yggdrasil 服务端技术规范 § 使用角色名称登录](Yggdrasil-服务端技术规范#使用角色名称登录)
* 账户所对应角色的 UUID
仅当两个账户的以上三个属性都相同时,这两个账户才是相同的,其中一个属性相同并不能代表两账户相同。同一验证服务器上可以存在多个角色;多个角色可以属于同一个用户;不同验证服务器上也可以出现具有相同 UUID 的角色。因此,启动器应**同时**使用这三个属性来标识账户。
除了以上三个属性外,账户还具有以下属性:
* 令牌accessToken 及 clientToken
* 账户所对应角色的名称
* 用户的 ID
* 用户的属性
> 安全警告:
> * 记住登录状态记录的是令牌,**不是**用户的密码。密码在任何时候都**不应该**被明文存储。
以上属性都是可变的。每次登录或刷新操作后,启动器都需要更新存储的账户属性。
在下面所有的登录和刷新操作中,请求中 `requestUser` 参数都为 `true`,这样启动器便能够即时更新用户 ID 和用户属性。
## 账户的添加
如果用户要添加一个账户,则启动器需要询问用户使用的验证服务器、用户的账户和密码。这里的验证服务器,可以是预先设置好的,可以是用户从验证服务器列表中选择的,也可以是用户即时设置的([见上文](#验证服务器的设置))。
此后,启动器进行以下操作:
1. 调用相应验证服务器的[登录接口](Yggdrasil-服务端技术规范#登录),其中包含用户输入的账户和密码。
2. 如果响应中 `selectedProfile` 不为空,则登录成功,使用响应中的信息更新账户属性。流程结束。
3. 如果响应中 `availableProfiles` 为空,则用户没有任何角色,触发异常。
4. 提示用户从 `availableProfiles` 中选择一个角色。
5. 调用[刷新接口](Yggdrasil-服务端技术规范#刷新),其中的令牌为登录操作所返回的令牌,`selectedProfile` 为上一步中用户选择的角色。
6. 登录成功,使用刷新响应中的信息更新账户属性。
## 凭证有效性的确认
启动器在使用凭证前(例如启动游戏前),需要确认其有效性。如果凭证失效,则需要用户重新登录。确认凭证有效性的步骤如下:
1. 调用[验证令牌接口](Yggdrasil-服务端技术规范#验证令牌),其中包含账户的 accessToken 和 clientToken。
2. 如果请求成功,则当前凭证有效,流程结束。否则继续执行。
3. 调用[刷新接口](Yggdrasil-服务端技术规范#刷新),其中包含账户的 accessToken 和 clientToken。
4. 如果请求成功,则使用刷新响应中的信息更新账户属性,流程结束。否则继续执行。
5. 启动器要求用户重新输入密码。
6. 调用[登录接口](Yggdrasil-服务端技术规范#登录),其中包含用户的账户和上一步输入的密码。
7. 如果登录响应中 `selectedProfile` 不为空,则:
1. 如果 `selectedProfile` 中的 `uuid` 与账户对应角色的 UUID 相同,则使用登录响应中的信息更新用户属性。流程结束。
2. 触发异常(原账户的角色已不可用)。
8. 从 `availableProfiles` 中找出 UUID 与账户对应角色相同的角色。如果没有,则触发异常(原账户的角色已不可用)。
9. 调用[刷新接口](Yggdrasil-服务端技术规范#刷新),其中令牌为登录操作返回的令牌,`selectedProfile` 为上一步中所找到的角色。
10. 登录成功,使用刷新响应中的信息更新账户属性。
## 账户信息的显示
启动器在显示账户时,除了账户对应角色的名称之外,还应该显示账户所属的验证服务器([见上文](#服务器名称的显示)),防止用户混淆不同验证服务器上的同名角色。
如果启动器要显示角色皮肤,则可以调用[查询角色属性接口](Yggdrasil-服务端技术规范#查询角色属性)获取角色属性,角色属性中包含了[角色的皮肤信息](Yggdrasil-服务端技术规范#角色信息的序列化)。
# 启动游戏
启动器在启动游戏前需要进行以下工作:
1. (若需要)[下载 authlib-injector](#下载-authlib-injector)
2. [确认凭证有效性](#凭证有效性的确认)
3. [配置预获取](#配置预获取)
4. [添加启动参数](#添加启动参数)
其中第 1、2、3 步可以并行执行,以提升启动速度。
## 下载 authlib-injector
启动器可以自带 authlib-injector.jar也可以在启动游戏前下载一个并缓存。本项目提供了[一个 API](获取-authlib-injector#下载-api) 用于下载 authlib-injector。
如果你的用户主要在中国大陆,我们推荐你从 [BMCLAPI 镜像](获取-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 格式)(若启动器不支持,可替换为 `{}`|

View file

@ -0,0 +1,84 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
目录
=================
- [获取 authlib-injector](#%E8%8E%B7%E5%8F%96-authlib-injector)
- [原版服务端、Spigot 等](#%E5%8E%9F%E7%89%88%E6%9C%8D%E5%8A%A1%E7%AB%AFspigot-%E7%AD%89)
- [BungeeCord](#bungeecord)
- [调用 Mojang 皮肤](#%E8%B0%83%E7%94%A8-mojang-%E7%9A%AE%E8%82%A4)
- [通过代理访问 Mojang](#%E9%80%9A%E8%BF%87%E4%BB%A3%E7%90%86%E8%AE%BF%E9%97%AE-mojang)
- [兼容性](#%E5%85%BC%E5%AE%B9%E6%80%A7)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
本文主要介绍如何在 Minecraft 服务端使用 authlib-injector。
## 获取 authlib-injector
首先你需要从[此处](https://authlib-injector.yushi.moe/)下载最新版本的 authlib-injector。
## 原版服务端、Spigot 等
请将服务端的 `online-mode` 设置为 `true`,然后在其启动命令中添加以下 JVM 参数:
```
-javaagent:{path/to/authlib-injector.jar}={https://your-yggdrasil-api-root.com}
```
- `{path/to/authlib-injector.jar}` 表示你在[上一步](#获取-authlib-injector)中下载的 JAR 文件所在的位置(相对路径、绝对路径皆可)。
- `{https://your-yggdrasil-api-root.com}` 表示验证服务器的 URL。
例如,这是原先的启动命令:
```
java -jar minecraft_server.1.12.2.jar nogui
```
假设:
- 你下载到的 authlib-injector JAR 文件名为 `authlib-injector.jar`
- 你将其放到了与服务端 JAR `minecraft_server.1.12.2.jar` 相同的目录下。
- 验证服务器的 URL 为 `https://example.yggdrasil.yushi.moe`
那么添加参数后的命令行应该如下:
```
java -javaagent:authlib-injector.jar=https://example.yggdrasil.yushi.moe -jar minecraft_server.1.12.2.jar nogui
```
## BungeeCord
如果使用 BungeeCord那么在所有服务端上都需要加载 authlib-injector[方法见上](#原版服务端spigot-等)),但应只有 BungeeCord 打开 `online-mode`,后端 MC 服务端应关闭 `online-mode`
## 调用 Mojang 皮肤
加载 authlib-injector 后,所有皮肤默认都是从指定的验证服务器处获取的。例如:
* `/give @p minecraft:skull 1 3 {SkullOwner:"notch"}`
* Citizens2 插件)`/npc skin notch`
这些命令获取的都是**自定义的验证服务器上**名为 `notch` 的角色的皮肤。
如果要使用 Mojang 的皮肤,则可以在角色名称后加上 `@mojang`,如:
* `/give @p minecraft:skull 1 3 {SkullOwner:"notch@mojang"}`
* `/npc skin notch@mojang`
详细说明见 [README § 参数](https://github.com/yushijinhun/authlib-injector#参数) 中的 `-Dauthlibinjector.mojangNamespace` 选项。
### 通过代理访问 Mojang
调用 Mojang 皮肤的功能需要 MC 服务端能够访问 Mojang API。如果你的服务端要通过代理才能访问 Mojang那么你可以在启动时添加以下 **JVM 参数**来指定代理:
```
-Dauthlibinjector.mojangProxy=socks://<host>:<port>
```
注意:
* 只有向 Mojang 查询角色信息时才会使用此代理,材质图像下载不走代理(即使是来自 Mojang 的材质)。
* 目前仅支持 SOCKS5。
## 兼容性
一般而言authlib-injector 兼容绝大多数插件和 Mod。下表列出的是曾经存在兼容性问题的插件 / Mod / 服务端:
|受影响的插件 / Mod / 服务端|受影响的 authlib-injector 版本|备注|
|----|---|----|
|Citizens2|<=1.1.23|[#27](https://github.com/yushijinhun/authlib-injector/issues/27) [#28](https://github.com/yushijinhun/authlib-injector/pull/28)|
|LaunchWrapper|=1.1.24|[#33](https://github.com/yushijinhun/authlib-injector/issues/33)|
|ModLauncher|1.1.24, 1.1.25|[#38](https://github.com/yushijinhun/authlib-injector/pull/38)|
|Arclight|<=1.1.30|[#80](https://github.com/yushijinhun/authlib-injector/issues/80)|
|Geyser (plugin)|<=1.1.31|[#83](https://github.com/yushijinhun/authlib-injector/issues/83)|

43
docs/签名密钥对.md Normal file
View file

@ -0,0 +1,43 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
目录
=================
- [概述](#%E6%A6%82%E8%BF%B0)
- [密钥对的生成和处理](#%E5%AF%86%E9%92%A5%E5%AF%B9%E7%9A%84%E7%94%9F%E6%88%90%E5%92%8C%E5%A4%84%E7%90%86)
- [生成私钥](#%E7%94%9F%E6%88%90%E7%A7%81%E9%92%A5)
- [从私钥生成公钥](#%E4%BB%8E%E7%A7%81%E9%92%A5%E7%94%9F%E6%88%90%E5%85%AC%E9%92%A5)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# 概述
本文主要介绍用于数字签名的密钥对。文中将使用 OpenSSL 对密钥进行操作。
验证服务器会对以下请求的响应中的角色属性进行[数字签名](Yggdrasil-服务端技术规范#角色信息的序列化)
* [服务端验证客户端](Yggdrasil-服务端技术规范#服务端验证客户端)
* [查询角色属性](Yggdrasil-服务端技术规范#查询角色属性)(仅当 `unsigned=false` 时才需要)
验证服务器通过[API 元数据](Yggdrasil-服务端技术规范#api-元数据获取)将公钥公开,以便 authlib-injector 获取。
注意:验证服务器应避免密钥的变化。如果使用多个服务端实例进行负载均衡,那么它们应该使用同一个密钥。
# 密钥对的生成和处理
下面对 OpenSSL 的调用都是使用标准输入和标准输出进行输入输出的。
如果要使用文件,可使用参数 `-in <file>``-out <file>`
## 生成私钥
密钥算法为 RSA推荐长度为 4096 位。
```
openssl genrsa 4096
```
生成的私钥将输出到标准输出。
## 从私钥生成公钥
```
openssl rsa -pubout
```
私钥从标准输入读入,公钥将输出到标准输出。

View file

@ -0,0 +1,63 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
目录
=================
- [手动下载](#%E6%89%8B%E5%8A%A8%E4%B8%8B%E8%BD%BD)
- [下载 API](#%E4%B8%8B%E8%BD%BD-api)
- [获取版本列表](#%E8%8E%B7%E5%8F%96%E7%89%88%E6%9C%AC%E5%88%97%E8%A1%A8)
- [获取特定版本](#%E8%8E%B7%E5%8F%96%E7%89%B9%E5%AE%9A%E7%89%88%E6%9C%AC)
- [获取最新版本](#%E8%8E%B7%E5%8F%96%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)
- [BMCLAPI 镜像](#bmclapi-%E9%95%9C%E5%83%8F)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# 手动下载
authlib-injector 的最新版本可以直接从 [authlib-injector.yushi.moe](https://authlib-injector.yushi.moe/) 下载。
# 下载 API
authlib-injector 项目提供了一组 API 用于下载 authlib-injector 构件。其 API 入口为 [`https://authlib-injector.yushi.moe/`](https://authlib-injector.yushi.moe/)。
## 获取版本列表
`GET /artifacts.json`
响应格式:
```javascript
{
"latest_build_number": 最新版本的构建号,
"artifacts": [ // 各版本信息
{
"build_number": 此版本的构建号,
"version": "此版本的版本号"
}
// ,... (可以有更多)
]
}
```
## 获取特定版本
`GET /artifact/{build_number}.json`
URL 中的 `{build_number}` 参数代表版本构建号。
响应格式:
```javascript
{
"build_number": 此版本的构建号,
"version": "此版本的版本号",
"download_url": "此版本的 authlib-injector 的下载地址",
"checksums": { // 校验和
"sha256": "SHA-256 校验和"
}
}
```
## 获取最新版本
`GET /artifact/latest.json`
响应格式[同上](#获取特定版本)。如果你只需要获取最新版本,使用此 API 足矣。
## BMCLAPI 镜像
> 使用 BMCLAPI 时请遵守 [BMCLAPI 的协议](https://bmclapidoc.bangbang93.com/#api-_)。
BMCLAPI 为本下载 API 提供了一个[镜像](https://bmclapidoc.bangbang93.com/#api-Mirrors-Mirrors_authlib_injector),其入口为 [`https://bmclapi2.bangbang93.com/mirrors/authlib-injector/`](https://bmclapi2.bangbang93.com/mirrors/authlib-injector/)。