由于我的一个朋友最近厌倦了网易的MC,想要体验一下之前玩过的Java版, 还想玩AoA3这个模组玩刺激的RPG。 正好我手头有空闲的计算资源,用本文来记录一下搭建过程和遇到的问题。

准备工作

调研的时候自然是优先打算采取Docker方案的, 毕竟上次开服务器的时候Docker证明了它的易用性和稳定性。

但是很快遇到了和搭建CI系统时一样的问题, 我用于搭建MC服务器的主机并非我家里的主机一样有透明代理, 本地代理显然也不是一个优雅的解决方案, 而且该主机上面也挂了不少服务,考虑之下还是上传jar包手动搭建。

由于这个朋友的朋友们都是萌新肝帝,有正版的人很少数, 再考虑到之前搭建MC服务器的时候有被顶替爆破的问题, 最后还是顺手又搭建了一个基于Blessing Skin的皮肤站, 并开启了验证码、正版验证 和世界树API三个功能。

游戏要求

AoA3模组在MCWiki下列出的一般要求是:

  • MineCraft Java Edition 1.16.5
  • Forge

MC 1.16.5版本主要使用Java8运行, 但是通过ModernFix这个模组, 可以让1.16版本的MC在更改虚拟机参数的前提下使用Java17运行, 所以这里服务端和客户端都采用Java17。

也是因为Debian Bullseye已经不提供Java8的JRE包了

游戏服务器

搭建Minecraft服务器用的主机是一台Debian Bullseye的LXC虚拟机,没有Java环境。

安装Java环境

Java环境可以从Oracle的Adoptium还有IBM的openj9当中选择。 清华大学提供了Adoptium的Repo镜像, 以及openj9的Github Release镜像, 或者直接从Debian Bullseye源安装。

从Debian源安装 openjdk-17-jre-headless
$ sudo apt install openjdk-17-jre-headless

查看实际的Java文件执行位置:

$ readlink $(readlink /usr/bin/java)
/usr/lib/jvm/java-17-openjdk-amd64/bin/java

两次readlink是因为Debian会将Java的可执行文件/usr/bin/java 连接到版本控制软链接/etc/alternatives/java上。

创建用户

用户目录放在/opt/minecraft下。

$ sudo mkdir -p /opt/minecraft
$ sudo useradd -M -d /opt/minecraft -s /bin/bash mc-server 
$ sudo chown -R mc-server:mc-server /opt/minecraft
$ sudo su - mc-server

可执行文件下载

皮肤站

依赖要求

新版Blessing Skin需要PHP8.0和一个后端数据库,这里选择Postgresql,并用Redis作为缓存。

Debian Bullseye的仓库里面并没有PHP8(虽然Bookworm已经支持了), 所以需要第三方源安装,这里选取的是sury的第三方PHP仓库。

安装第三方源:

$ sudo wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
$ sudo echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list
$ sudo apt update

安装相应的PHP依赖:

$ sudo apt install php8.0-fpm php8.0-zip php8.0-gd php8.0-mbstring php8.0-xml php8.0-redis

为Blessing Skin创建Postgres数据库:

$ sudo -u postgres psql

psql (13.9 (Debian 13.9-0+deb11u1))
Type "help" for help.
postgres=# CREATE ROLE minecraft WITH LOGIN PASSWORD 'APASSWORD';
postgres=# CREATE DATABASE blessingskin WITH OWNER minecraft TEMPLATE template0 ENCODING UTF8 LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8';

调整数据库访问权限:

$ sudo -e /etc/postgresql/13/main/pg_hba.conf
host    blessingskin    minecraft       127.0.0.1/32            scram-sha-256
local   blessingskin    minecraft                               scram-sha-256

为访问Postgresql数据库的php用户加上postgres和redis的unix socket访问权限:

$ sudo usermod -aG pgsock www-data
$ sudo usermod -aG redis www-data

可执行文件下载

搭建过程

游戏服务器

安装server和forge

将服务器放在/opt/minecraft/aoa3-1.16.5目录下,Forge安装包放在/opt/minecraft下。

新建一个原版服务器:

$ pwd
/opt/minecraft
$ pushd aoa3-1.16.5
$ java -jar server.jar nogui

在服务器成功运行之后,使用Ctrl-C停止,

安装Forge:

$ popd
$ java -jar forge-1.16.5-36.2.39-installer.jar --installServer aoa3-1.16.5 nogui

该过程需要稳定的网络连接。

安装完成之后,就可以删除Forge的安装包,并且配置server.properties和放置mod。

编写启动脚本

由于使用Java17启动Minecraft 1.16.5 + Forge,所以必须对虚拟机参数做一定的修改, 并且限制MC本身分配的内存。

#!/bin/sh
/usr/lib/jvm/java-17-openjdk-amd64/bin/java \
	-Xms4G -Xmx8G \
	-Djava.security.manager=allow \
	-Dfile.encoding=UTF-8 \
	--add-opens java.base/jdk.internal.loader=ALL-UNNAMED \
	--add-opens java.base/java.net=ALL-UNNAMED \
	--add-opens java.base/java.nio=ALL-UNNAMED \
	--add-opens java.base/java.io=ALL-UNNAMED \
	--add-opens java.base/java.lang=ALL-UNNAMED \
	--add-opens java.base/java.lang.reflect=ALL-UNNAMED \
	--add-opens java.base/java.text=ALL-UNNAMED \
	--add-opens java.base/java.util=ALL-UNNAMED \
	--add-opens java.base/jdk.internal.reflect=ALL-UNNAMED \
	--add-opens java.base/sun.nio.ch=ALL-UNNAMED \
	--add-opens jdk.naming.dns/com.sun.jndi.dns=ALL-UNNAMED,java.naming \
	--add-opens java.desktop/sun.awt.image=ALL-UNNAMED \
	--add-modules jdk.dynalink \
    --add-opens jdk.dynalink/jdk.dynalink.beans=ALL-UNNAMED \
	--add-modules java.sql.rowset \
	--add-opens java.sql.rowset/javax.sql.rowset.serial=ALL-UNNAMED \
	--add-exports java.base/sun.security.util=ALL-UNNAMED \
	--add-opens java.base/java.util.jar=ALL-UNNAMED \
	-javaagent:authlib-injector-1.2.2.jar=https://mc.azurcrystal.com/api/yggdrasil \
	-jar forge-1.16.5-36.2.39.jar nogui

之后可以根据这个启动脚本编写systemd文件或者使用tmux直接管理, 由于MC服务器并不要求自动启动和自动重启, 而且为了方便快捷的使用forge内置的Shell,我这里选择了tmux。

皮肤站

检查PHP环境

首先查看php-fpm是否在正常运行:

$ sudo systemctl status php8.0-fpm

查看php-fpm默认的unix-socket位置:

$ grep "listen =" /etc/php/8.0/fpm/pool.d/www.conf 
listen = /run/php/php8.0-fpm.sock
;pm.status_listen = 127.0.0.1:9001

当然,也可以为站点独立配置一个php-unixsocket:

$ cp /etc/php/8.0/fpm/pool.d/www.conf /etc/php/8.0/fpm/pool.d/mc.conf
$ sed -i s/php8.0-fpm.sock/blessingskin.sock/g /etc/php/8.0/fpm/pool.d/mc.conf

配置站点

将站点文件放置于/opt/www/blessingskin/下, 复制环境文件并生成设置密钥:

$ unzip -d /opt/www/blessingskin blessing-skin-server-6.0.2.zip
$ pushd /opt/www/blessingskin
$ cp .env.example .env
$ php artisan key:generate

配置.env

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=blessingskin
DB_USERNAME=minecraft
DB_PASSWORD=APASSWORD
DB_PREFIX=
...

BlessingSkin访问Postgresql的unix socket会出现无法插入的问题, 这里使用IP:Port作为临时解决方案。

...
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=sync

REDIS_CLIENT=phpredis
REDIS_SCHEME=unix
REDIS_SOCKET_PATH=/var/run/redis/redis-server.sock
REDIS_PASSWORD=null

并没有向站内用户发信的需求,所以直接使用sync节省资源。

配置nginx虚拟主机

在配置之前使用acme.sh申请了ecc的域名证书。

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name mc.example.com;
    ssl_certificate /path/to/ecc.crt;
    ssl_certificate_key /path/to/ecc.key;
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5;
    ssl_protocols TLSv1.3;
    ssl_session_timeout 5m;
    ssl_prefer_server_ciphers on;
    add_header Strict-Transport-Security "max-age=63072000" always;
    root /opt/www/blessingskin/public;
    location / {
        index  index.php index.html index.htm;
        try_files $uri $uri/ /index.php?$query_string;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }

    location ~ [^/]\.php(/|$){
        # try_files $uri =404;
        fastcgi_pass  unix:/run/php/php8.0-fpm.sock;
        include fastcgi.conf;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root/$fastcgi_script_name;
    }
}

后续优化

皮肤站

镜像插件市场

鉴于插件市场的迁移,且git.qvq.network在国内的连接并不理想, 遂在服务器端本地复制了一份插件市场, 并将其registry_zh_CN.json和registry_en.json中的registry地址进行替换:

$ sed -i s/git.qvq.network\/bs-community\/plugins-dist\/-/git.azurcrystal.com\/AzurCrystal\/plugins-dist/g registry_en.conf
registry_zh_CN.conf

相当于在本地镜像了一份插件市场,之后更改.env:

PLUGINS_REGISTRY=https://git.azurcrystal.com/AzurCrystal/plugins-dist/raw/master/registry_{lang}.json

之后只需要定时向上游拉取并自动打上Patch就可以在本地镜像了。

正版验证

由于劫持了世界树API,实际上正版验证在这个环节中已经成为可有可无的部分了。 在yggdrasil api由皮肤站运营的前提下,所有通过皮肤站账户连入的用户都是正版, 唯一的区别可能就是使用的UUID与实际正版账户不同了。 保持各个采用世界树API的UUID不同是一个明智的选择。

环境配置

为了启用正版验证,需要在.env中配置以下三个值:

MICROSOFT_KEY=9fce0559-44b4-4c95-a144-d3ccf50ea62b
MICROSOFT_SECRET=secret@123
MICROSOFT_REDIRECT_URI=https://skin.bs-community.dev/mojang/callback
注册微软应用

打开https://aka.ms/aad后,实际上会跳转到https://azure.microsoft.com/en-us/products/active-directory/, 也就是微软官方的身份验证服务。

点击登录(Signin) 并用微软账户登录以后,可以进到主控制台界面。

找到顶部的Azure服务中的Azur Active Directory,进入后选择添加-应用注册,输入应用名称(随意输入); 勾选受支持的账户类型为以下的其中一项,

  • 任何组织目录(任何 Azure AD 目录 - 多租户)中的帐户和个人 Microsoft 帐户(例如,Skype、Xbox)
  • 仅 Microsoft 个人帐户

并将重定向URI设置为Web,值为https://mc.example.com/mojang/callback。

之后点击完成创建,完成创建后的应用的应用程序(客户端)ID即是MICROSOFT_KEY。

MICROSOFT_SECRET则需点击左侧列表中的证书和密码,新建一个客户端密码,其值(非机密ID)即为变量值。 需注意,客户端密码值出现一次后就无法再查看,请及时保存。

之后按照所设置的变量更改.env,即可启用基于Microsoft账户的皮肤站内正版验证。

问题

皮肤站

  • 无法直接从皮肤站的上传页面上传皮肤
    • 推测是php8的处理问题,正在考虑编译一份php8.0.2
    • 目前可以直接从启动器中上传皮肤
  • 正版验证无法更改玩家UUID
    • 属于良性BUG
    • 更改UUID会导致玩家存档消失,而且容易混淆,反而不应当更改
    • 如需正版皮肤和披风应当使用CustomSkinLoader这个插件