「转载」又是一种 Minecraft 外置登录解决方案:自行实现 Yggdrasil API
原文链接:PRIN BLOG - 又是一种 Minecraft 外置登录解决方案:自行实现 Yggdrasil API
共享协议:本文与原文采用相同的 CC BY 4.0 协议而非本博客默认的共享协议
最近给 Blessing Skin 写了个插件,利用皮肤站本身的账号系统实现了 Yggdrasil API(就是 Mojang 的登录 API),然后配合 authlib-injector 这个项目将启动器(基于 Java 编写的支持正版登录的启动器都行)、Minecraft 游戏、Minecraft 服务端中的 Mojang Yggdrasil API 地址给替换成了自己实现的第三方 Yggdrasil API 地址(字节码替换),从而实现了与正版登录功能几乎完全相同的账户鉴权系统。
通俗地讲,就是我把 Mojang 的正版登录 API 给【劫持】成自己的啦,所以可以像登录正版那样直接用皮肤站的邮箱和密码登录游戏(还支持 Mojang 都不支持的多用户选择哦)。这种外置登录系统的实现应该可以说是比市面上的软件都要完善(毕竟可以直接利用 Minecraft 本身自带的鉴权模块),因此写一篇博文介绍一下这些实现之间的不同之处,顺带记录一下实现 Yggdrasil API 时踩到的坑,算是抛砖引玉了。
注意:本文不适合小白及问题解决能力弱的人群阅读。
感觉我明明好久没玩 MC 了,要玩也都是玩正版服务器,但是却一直在搞这些盗版服用的东西,我真是舍己为人造福大众普惠众生啊(不
服务器内置登录插件
相信维护过 Minecraft 服务器(当然,我这边说的是运行在离线模式下的服务器)的腐竹们或多或少都听说过 Authme、CrazyLogin 等登录插件的鼎鼎大名吧。由于这些服务器运作在离线模式(online-mode=false
,即俗称的盗版模式)下,缺少 Mojang 官方账户认证系统的支持,所以必须使用这类插件来进行玩家认证(否则随便谁都可以冒名顶替别人了,换一个登录角色名就行)。
这类插件的工作原理就是在服务端维护一个数据表,表中每一条记录中存储了角色的「角色名」、「登录密码」、「注册时间」、「登录 IP 地址」等等信息,当玩家初次进入服务器时需要通过这些插件进行注册操作(e.g. /register
命令)并在表中插入一条记录,注册完毕后进入服务器则需要输入密码(e.g. /login <password>
命令)来认证。
其实这样的解决方案也没什么不好,而且现在 Authme 等登录插件在众多的服务器中都还是主流。但是,如果你的服务器已经发展到比较大型了,或许你就比较希望有这样一个东西:
- 可以直接在启动器中进行登录鉴权操作,点击「开始游戏」就可以直接进入服务器,不用在游戏里再一遍遍输入
/login
等指令; - 有一个网页版的用户管理,可以直接对玩家进行操作(e.g. 封禁、修改积分);
- 玩家们可以直接在一个直观的网页上注册账号,并且可以直接用这个账号 & 密码登录游戏;
- 希望这个账号系统还能对接论坛、皮肤站等乱七八糟的东西,玩家注册了一个账号之后,可以在任何地方使用;
- 希望服务器有一个自己的网页、自定义启动器、用户管理系统、卫星地图之类的东西来装逼;
- etc.
并不是所有腐竹都满足于 Authme + Discuz 这样的组合的(而且这类游戏内登录系统也有不少安全漏洞),毕竟在这个 Minecraft 多人联机服务器发展接近饱和的时候,如果想要你的服务器能够吸引新玩家,那么除了服务器本身建设之外的地方也是要好好考虑的。
外置登录系统
正是这样的需求催生了不少 Minecraft 的「外置登录插件」、「网页登录」等等软件(而且人气都挺高的),我随手在 MCBBS 上一搜就有很多类似的产品,用啥语言写的都有:MadAuth、WebLogin、BeeLogin、WebRegister、冰棂登陆系统……
这些软件的原理就是将原本的登录鉴权这一步骤从游戏里抽出来了,将其放到启动器 or 网页上去,而服务端插件的功能就只剩下「查询数据库中用户的登录状态,决定是否放行」:
▲ 随手画的示意流程图,这里推荐一下 ProcessOn 这个在线作图网站,很好用 ;)
Printempw 的图是透明的,对灯箱显示不是很友好,所以就自己上了一层白色底
似乎也挺好的,不是吗?那我今天要说的「自行实现 Yggdrasil API」方法,和这些现成的方式有什么不一样呢?
自行实现 Yggdrasil API
继续看下去之前,首先你要知道 Mojang 正版的 Minecraft 是怎样登录的。Mojang 专门定义了一个用于鉴权的 API,Mojang 旗下的游戏(Minecraft、Scrolls 等)都是用的这一套 API 来正版验证的 —— 这一套 API 的名字就叫做 Yggdrasil(即北欧神话里的世界树,这名字可真几把炫酷)。
正版登录的好处就不用我说了吧?再也不用担心假人压测、自带外置登录(启动器里账号密码登录)、自带皮肤加载(不需要安装 CSL、USM 等皮肤补丁了)、Tab 栏显示头像……可以说,Minecraft 自带的 Yggdrasil API 鉴权系统比上面的那些什么登录插件啊什么外置登录的功能强多了,所以正版服务器(online_mode=true
)也不用担心那些破事,因为官方的这一套鉴权系统以及很完善了。
那么问题来了,盗版用户要怎样才能把 Mojang 为正版开发的 Yggdrasil API 系统拿来用呢?
基本原理
这里必须感谢 to2mbn/authlib-injector 这个项目,正是因为这个项目,我接下来描述的方法才成为可能。是的,方法很简单,Minecraft 虽然把 Mojang 官方的 Yggdrasil API 地址(https://authserver.mojang.com
)给写死在源码里了,但是既然 Minecraft 是基于 JVM 的应用程序,我们就可以通过字节码替换的方法将官方的 API 地址替换成我们自己实现的 API 地址。
以下内容援引自 authlib-agent(即 authlib-injector 前身)的 wiki:
authlib-agent 是一个高可靠性, 高适用性, 用于 Minecraft 的, 游戏外登录及皮肤解决方案. 支持 Minecraft1.7+, Craftbukkit, Spigot, Bungeecord 等. 通过对正版登录 API 的重定向, 实现了一个功能和正版几乎一样的游戏外登录系统.
不过既然要把官方 API 地址替换成我们自己的,我们就得自己实现一个和官方 API 其他地方都一样的 API,也就是,仿造出一个第三方 Yggdrasil API 出来。
解决方案
可以说这个系统中,就是「开发完整实现了 Yggdrasil API 的后端」这一步最难了。为啥捏?这个服务端不止要实现用户的认证、皮肤获取,你还得实现用户的注册、登录、角色管理、皮肤上传、皮肤库等等七七八八的功能吧?你还得给这些功能套上一个好看的界面吧,不然你让你的玩家怎么使用?你还得来个后台管理页面吧,不然管理员怎么进行用户管理、封禁等操作?
authlib-injector 官方也提供了一个 Java 编写的后端 yggdrasil-mock,虽然完整实现了 Yggdrasil API,但是它并没有提供直观的管理网页,只提供了一套 RESTful API,所以距离实装要求还是差得比较远的。
要重头开发一套这样的系统是非常非常够呛的,不过幸运的是,我之前一直在持续开发的 Minecraft 皮肤站 Blessing Skin Server,这个项目的 v3 版本正好就满足的这些要求 —— 友好的用户界面、完善的用户系统、强大的后台管理、附带皮肤上传管理展示功能,再加上我之前开发的插件系统(开发这玩意真是个正确的决定,一劳永逸啊),这让我可以很方便地开发一个插件出来,直接基于现成的皮肤站用户系统实现 Yggdrasil API。
如何使用
讲了那么多,那么到底该怎么使用呢?
请参阅:printempw/yggdrasil-api wiki。
以上步骤完成后你将得到什么?
- 一个完善的账号系统(配合数据对接插件还能与 Discuz 等论坛账号互通),包括友好的注册、登录网页界面以及强大的管理员面板,在管理后台中封禁用户后,该用户也将无法登录游戏;
- 一个皮肤管理系统,自带皮肤库功能,在皮肤站中应用的皮肤,玩家无需安装任何皮肤 Mod,进入游戏即可看到自己设置的皮肤(支持双层皮肤、支持 Alex 模型,由于游戏本身限制不支持高清皮肤);
- 单账户多角色功能,玩家可以像登录正版那样用「邮箱」和「密码」登录游戏,而且如果你在皮肤站中添加了多个角色的话,还可以在启动页面选择要用哪个角色进入游戏(Yggdrasil API 实现了这个功能,但是 Mojang 的正版登录服务器并未实现该功能),HMCL 等启动器都实现了本功能;
这还不够多吗?
而且你还可以自己修改 HMCL 等开源启动器的源码,在启动时自动注入 -javaagent
参数,更加方便,还能得到一个服务器专用启动器,逼格更高了(笑)
实现效果
皮肤站的用户管理系统、皮肤系统、后台界面之类的我就不截图了,有兴趣可以去 MCBBS 的 发布帖 上感受一下。
▲ 在皮肤站「角色管理」中可添加多个角色
▲ 使用皮肤站的邮箱与密码登录后,配合 HMCL 实现多角色选择
▲ 游戏内的显示效果
Yggdrasil API 踩坑记录
下面记录一些自己实现 Yggdrasil API 时踩到的坑,毕竟 wiki.vg 里并不会提到这些在自己实现 API 时需要注意的东西(提到的大部分都是使用 API 时应该要注意的),所以我也只能摸着石头过河,踩了不少坑,这里记录一下,希望能帮到后来人。
基础的 API 定义之类的我就不说了,下面主要讲一些 文档 里没怎么提到的东西。
2018-02-22 加注:
最近 @yushijinhun 写了一篇 Yggdrasil 服务端技术规范,大部分 API 相关的内容其中都有提及,大家去看那个就好了。
登录与鉴权
用过正版 Minecraft 的登录系统的同学应该都知道,一般只有在初次登录游戏或者太久没有开过游戏的情况下,启动器才会要求你输入账号密码,其他情况下都是可以直接点击登录并启动游戏的。
但这并不是因为启动器记下了你的密码,相反,启动器保存的是 Mojang 认证服务器返回的 AccessToken。如果你曾经观察过启动器启动游戏时所用的启动参数,你就能发现其实 Minecraft 游戏本体其实只拿到了角色名、角色 Profile 对应的 UUID 以及上面提到的 AccessToken 而已。可以说,只要拿到这个 AccessToken 就可以进行几乎所有的操作了。
1 |
|
获取 AccessToken
用正确的 username
和 password
请求 /authenticate
API 即可拿到 AccessToken,该令牌的有效期由服务端来决定(一般用 Redis 实现)。如果你请求 API 的时候没有带上 clientToken
,那服务端就会帮你生成一个,你要记得把这个返回值记下来,因为 clientToken 和 accessToken 是对应关系,有些 API 是要求同时提供 AccessToken 和签发该令牌的 clientToken 的。
另外需要注意的是,这个 /authenticate
API 中请求体中的 username
字段,填的是邮箱。
是的,你没听错,email,在 username 字段里填的是用户的 email。惊不惊喜,意不意外?这个狗屎一样的字段命名估计和历史遗留问题也有关系,因为早期 Minecraft 账号(也就是 Profile 里的那个 legacy
字段)是直接用「角色名」和「密码」登录的,但是新版 Mojang 账号(Yggdrasil API)认证是用的「电子邮箱账号」,Yggdrasil API 为了兼容旧账号的登录,所以搞了这么一个坑爹的东西,真是说不出话。
总之,如果想要自己实现 Yggdrasil API,是要注意一下这个神秘的 username
字段的。
刷新 AccessToken
在登录成功拿到 accessToken
后,启动器应该把这个令牌存起来,然后在每次玩家登录游戏之前请求一次 /refresh
API,提供 accessToken 和签发该令牌时用的 clientToken(这也是我为什么上面叫你要把这个存起来的原因),就可以拿到新签发的 accessToken 了(刷新令牌有效期)。只要令牌有效期没过,启动器就不会再次请求 /authenticate
API。
所以,虽然文档上没说,但是其实 /refresh
返回的结果应该是要和 /authenticate
的返回结果大致相同的,包括 accessToken、clientToken、availableProfiles、selectedProfile、user 等字段(具体下面再说)。
API 中的 Token
Yggdrasil API 的定义中主要有两个 Token,clientToken
与 accessToken
,两者为对应关系。一般来说,启动器不会频繁变动 ClientToken(通常情况下,是永远不会变的),而 AccessToken 应该在每次登录游戏时通过 /refresh
重新签发一个。
Token 的生命周期
需要注意的是,AccessToken 是有生命周期的,大致如下:
1 |
|
AccessToken 刚签发时处于「有效」状态,经过一段时间后(服务端自行设置)变成「暂时失效」状态。在这种状态下的 AccessToken 是无法进入任何开启了正版验证的服务器的(也就是 /join
API 不认),但是该令牌还是能拿来请求 /refresh
API,这会签发一个全新的处于「有效」状态的 AccessToken 并返回给客户端。
但是如果处于「暂时失效」状态的 AccessToken 再放置一段时间后就会完全失效(一般的实现就是从 Redis 令牌桶中删掉该令牌),处于「无效」状态的 AccessToken 是无法进行任何操作的,只能让用户重新输入密码并请求 /authenticate
API 以获取一个新的 AccessToken。
Token 的格式
Yggdrasil API 中的 clientToken
、accessToken
、id
等字段的格式都是一大串 16 进制数字和 -
连字符组成的字符串,让人看起来很懵。其实这样的字符串格式就是 通用唯一识别码(Universally Unique Identifier)标准,也就是我们经常听到的 UUID 了。标准形式的 UUID 包含 32 位 16 进制数字,并且由连字符分割成形式为 8-4-4-4-12 的字符串,就像这样:
1 |
|
虽然文档中没说,但是 API 请求以及响应的 clientToken
、accessToken
以及玩家 Profile 中的 id
字段格式都是【不带连字符的 UUID】。下面拿 wiki.vg 中的 /authenticate
请求中的实例响应讲解一下:
1 |
|
至于后端存储时用怎样的格式就随意了,不过在 API 返回结果中是一定要按照上面的格式来的。
多角色选择功能
虽然 Mojang 官方迄今为止仍未支持同一个账号(Mojang 账号,用邮箱登录的那个)下添加多个角色(角色名,就是游戏里显示的那个),但是 Yggdrasil API 本身是可以实现这个「单账号多角色」功能的,并且官方启动器、HMCL 等著名的第三方启动器都支持登录后选择角色进入游戏(具体效果参见上方截图)。
如果你仔细阅读过 wiki.vg 里的 API 文档的话就会发现,/authenticate
里面有好几个包含了 Profile 的字段,分别是 availableProfiles
、selectedProfile
和 user
。下面我稍微说一下这几个字段的功能。
首先,availableProfiles
中存放的是这个账号下所有可用角色的 Profile,格式为 JSON 数组:
1 |
|
需要注意的是,每个角色 Profile 都应该有一个唯一的 id
(格式为不带连字符的 UUID),而不是每个账号一个。而且,虽然官方文档上没有写,其实 /refresh
API 返回的结果应该和 /authenticate
一样带上 availableProfiles
这个属性(因为只有第一次密码登录才会请求 /authenticate
,之后进游戏就只会请求 /refresh
了)。
而 selectedProfile
字段内容为被选中的角色 Profile。如果这个字段存在,启动器就会直接用这个角色进入游戏。只有在 selectedProfile
字段不存在时,启动器才会弹出「选择角色」对话框,并根据用户的输入选择不同的角色进入游戏。如果你想要搞支持单账户多角色的 API 的话,可以不用管这个字段(不过当该账号名下只有一个角色的话记得指定 selectedProfile
,这样启动器就可以直接用这个角色进游戏了)。
至于 user
字段是只有在请求时带上了 requestUser
属性时才会回复的,其中包括被选中角色的 UUID、语言偏好、Twitch 的 AccessToken 等等,一般来说,自己实现 Yggdrasil API 时可以忽略这玩意(而且这个属性对单账户多角色的支持并不好)。
加载皮肤与披风
这里稍微提一下 Minecraft 使用 Yggdrasil API 时加载皮肤的原理。
首先你要知道,Minecraft 游戏启动时从启动器那边(i.e. 从命令行)拿到的 API 相关属性只有「AccessToken」、「选中角色的 UUID」以及「选中角色的角色名」这三样东西。获取 Profile 以及加载皮肤是 Minecraft 游戏该做的工作,具体流程如下。
获取完整 Profile
首先 Minecraft 会请求 API /profiles/minecraft/{uuid}
获取角色的完整 Profile,差不多长这样:
1 |
|
好吧好吧,看到这么多字符先别懵,value
和 signature
字段的内容都是 BASE64 编码过的,解码后 value
字段就是个普通的 JSON 而已。至于 JSON 里是什么内容,就自己去看 wiki 吧。
数字签名
需要注意的是上述 Profile 中的 signature
字段。顾名思义,这个字段就是 value
字段的数字签名。虽然官方 API 只有在指定 unsigned=false
时才会返回带签名的 Profile,但是目前(截至本文发布)authlib-injector 在服务端未返回数字签名时会出现神秘的错误,所以还是默认返回 signature
字段来得好。
至于数字签名如何生成,其实就是用的 OpenSSL 内置的签名算法。各个平台都有 OpenSSL 库的实现,我这里贴一下 PHP 的示例代码:
1 |
|
其他语言大同小异,我就不多赘述了。
加载材质
拿到角色 Profile,并且验证了数字签名后(签名不对的话不会加载的),Minecraft 游戏就会根据 Profile 中指定的皮肤、披风图片 URL 加载材质。需要注意的是,Minecraft 自带的 authlib 是只会加载 Mojang 官方域名下的材质的(白名单之外的材质地址是不会被加载的),这也是为什么需要 CustomSkinLoader 等皮肤 Mod 的原因。不过 authlib-injector 自带了对 authlib 的 hack,在配置文件(或者远程配置加载)中直接指定材质加载白名单即可。
如果一切正常,游戏内就会显示你的自定义皮肤了。
加入服务器
在 Minecraft 中加入一个服务器时,客户端会向 /join
API 发出一个请求,请求体中包含了 AccessToken、当前角色的 UUID 以及服务器的唯一标识符 serverId
(这玩意如何生成不用我们操心,Minecraft 游戏里会搞好的,你只管存这个就行了)。
在后端实现上,一般来说就是在 Redis 这类内存数据库中放一个键值对,具体数据结构你自己想。
向 Yggdrasil API 发送完 join
请求后,Minecraft 客户端会向要加入的那个游戏服务器发送一个请求(这部分我们不用操心),服务器收到加入请求后,会向 Yggdrasil API 发送一个 hasJoined
请求(Query String 中包含角色名、IP 以及服务器唯一标识符),如果该用户已经加入了服务器(也就是判断数据库中有没有之前 join
时添加的记录),那就返回角色的完整 Profile,同时服务器允许用户进入。
这也就是为什么客户端和服务端同样需要使用 authlib-injector hack 的原因,因为我们要确保两者请求的都是同一个 API,这样才能起到一个维护登录状态的功能。
经常用到的 API
虽然 Yggdrasil 规范中定义了很多 API,但是其实日常游戏中用到的没几个,这里列举一些频繁使用的 API,也方便诸君知道哪里该认真开发哪里可以小小偷懒一下:
1 |
|
其他 API 感觉都是几万年用不到一次的,很神秘。
后记
上周折腾了好几天这玩意,写篇博文记录一下,既能理清自己的思路,还有可能帮到后来人(花时间研究了东西,却没人知道,多亏啊),何乐而不为呢 :P
参考链接
- http://wiki.vg/Authentication
- http://wiki.vg/Protocol_Encryption#Authentication
- http://wiki.vg/Mojang_API
- https://github.com/to2mbn/authlib-injector/
- https://zh.wikipedia.org/wiki/通用唯一识别码
- php-rsa - 加密解密和签名
- blessing-skin-plugins/yggdrasil-api
文章更新日志
具体的修改可以查看这篇博客在 GitHub 上源码的 历史提交记录。
2018-02-22:
- 基于最新的 authlib-injector 修改文章
- 将具体部署步骤移动至 yggdrasil-api 页面
- 同时也更新了 MCBBS 上的 相关帖子