Minecraftのホワイトリストをサーバ間で共有するシステムを作ってみた
概要
今回、初めてWebサービス(?)を開発しデプロイしたので、総括の意味も込めて記事にまとめることにしました。ただし、本当に内輪向けに開発したものなので大っぴらに公開する予定はありません。
システムの概要としては、タイトル通りホワイトリストを複数のMinecraftサーバ間で共有するもので、
比較的大規模かつ同一の組織に所属する人が、組織内の人間向けにMinecraftのサーバを公開したい場合などを想定しています。
Github: https://github.com/McOrgAuth
仕組み
ユーザの目線から本システムの仕組みを解説します。
登録
ユーザは、登録ページにアクセスしてメールアドレスとMinecraftのプレイヤー名を入力します。組織の認証は入力されたメールアドレスのドメイン部を正規表現でチェックすることで実現しています。登録を行うと仮登録IDが発行され、先程入力したメールアドレス宛に仮登録IDを含む確認用のリンクが送信されるので、利用者がそこにアクセスすることで登録が完了します。
認証
CitAuthPlugin(Minecraftサーバがユーザ認証を行うためのプラグイン)を導入しているサーバは、プレイヤーのログイン試行時にプレイヤーのUUIDをAPIサーバに問い合わせます。登録されている場合はそのままプレイヤーはログインできますが、登録されていない場合はキックされます。
設計
次に、システムの設計についてまとめます。
今回開発したシステムの設計は以下の通りになっています。図汚くてごめん!!作図法がいまいちわからねんだ
システムは4つのDockerコンテナから構成されており、リバースプロキシ兼Webサーバ用のコンテナにCloudflaredを立てて、Cloudflare Tunnel経由で外部からの接続を受け付けています。オリジンサーバのIPアドレスが隠蔽できるので便利ですね。
1. Reverse-Proxy and Web Server
使用したソフトウェアや言語、ライブラリなど
- nginx: v1.27.1
- php: v8.2.26
- apcu: v5.1.24
- php-fpm: v8.2
リバースプロキシ兼Webサーバです。外部からの接続を受け付けます。ユーザ登録ページを提供する他、Minecraftのサーバからのユーザ認証要求や、アクセストークンを管理する認可サーバへのアクセスなどを各サーバに転送するリバースプロキシの機能も持たせてあります。
2. API Server
使用したソフトウェアや言語、ライブラリなど
- nodejs: v22.5.1
- npm: v10.8.2
- express: v4.21.1
- nodemailer: v6.9.16
- config: v3.3.12
- jsonwebtoken: v9.0.2
ソースコード: https://github.com/McOrgAuth/citauth-api
APIサーバです。ユーザ認証要求やユーザ登録などを受け付け、後述のSYSサーバに転送します。また、メールアドレスの真正性を確認するため、ユーザが入力したメールアドレス宛に確認メールを送信します。
3. SYS Server
使用したソフトウェアや言語、ライブラリなど
- OpenJDK17: v17.0.14
- Apache Maven: v3.9.9
- JSON in Java: v20250107
- mysq-connector-j: v9.2.0
ソースコード: https://github.com/McOrgAuth/citauth-sys
SYSサーバです。データベース操作系の処理を行うサーバで、Javaで実装しています。一応外見上はSYSサーバとAPIサーバは2つで一つみたいな設計になっています。
4. Minecraft Plugin
使用したソフトウェアや言語、ライブラリなど
- OpenJDK 17: v17.0.14
- Apache Maven: v3.9.9
- spigot-api: v1.20.4-R0.1-SNAPSHOT
- JSON in Java: v20250107
ソースコード: https://github.com/McOrgAuth/citauth-mcplugin
Minecraftサーバ(Spigot)用のプラグインです。マイクラのユーザがログインした際、そのユーザがシステムに登録されているかを問い合わせる等の機能を有しています。
大変だったところ
正直全部大変だったのですが、特に実装・実現が大変だった部分について書きたいと思います。
ユーザ認証要求の際の処理並列化(CitAuthPlugin)
ユーザ認証要求をAPIに送信する際、Network I/Oが発生するのですが、このとき何も考えず実装したらMinecraftサーバのメインスレッドで処理を実行してしまいI/O待ちで世界が停止する問題が発生...
結局、ログイン時の問い合わせ処理はPlayerLoginEventからPlayerPreLoginEventにlistenするeventを変更することで一連の処理がUser Authenticatorスレッドというサーバスレッドとは別のスレッドで処理、コマンドラインからの問い合わせ処理はRunnableインタフェースをimplementsしてrunTaskAsynchronouslyでCraft Schedulerスレッドで処理、という感じで解決できました。
ただ、ここらへん結構試行錯誤したせいでログイン時の処理がApiConnectionクラス、コマンドラインからの入力時の処理がApiConnectionRunnableクラスと別クラスで実装という感じになっており非常によろしくないと思う。いつか直したい。
APIサーバとSYSサーバ間のメッセージ仕様
APIサーバとSYSサーバはTCP/IPでプロセス間通信を行っているのですが、この間のメッセージ仕様(プロトコルか?)を決めるのも大変でした。
途中迷走してメールアドレスとUUIDの間を#で区切ってどうたら~とかやってました。最終的に仮登録idとかも流す必要が出てきたので諦めてJSONにしました。
Webサーバ用のアクセストークンの保存場所
これも結構悩みました。最初は何も考えずWebリソースと同じディレクトリにtoken.json(アクセストークン)とcredential.json(認可サーバ用の認証情報)を置いていたのですが(あとから考えるととんでもねえことしてるな...)、流石にこれは駄目だろと後になって気づき、結局php-fpmのAPCuを用いてメモリ上に保存することにしました。環境変数に置くことも考えたのですが、setenvで設定した値はセッションが終了すると消えてしまうのでやめました。
反省点
APIサーバとSYSサーバを分ける必要はあったのかという疑問
今回、外部からの要求を受け付けるAPIサーバとデータベース処理を行うSYSサーバを分割して実装しました。理由はよく覚えていませんが、MySQLサーバと接続してデータベース操作するならJavaのがいいよなー、でもAPI実装するならNode.jsだよなーとかいう適当な理由だった気がします。(一つのシステムを異なる複数の言語を組み合わせて実装できたらかっけーなみたいなことも考えていた気がします。)
実際、複数の言語で実装することで各言語の理解は深まったのですが、開発効率を考えると分割しないほうがが良かった気もします。Node.jsはライブラリが豊富でmysql2といったMySQLと接続する際に使用できるライブラリなどもありJavaを使わなくても
TypeScriptで書けばよかった
今回のシステムはJavaScript(APIサーバ、認可サーバ)、Java(SYSサーバ、Minecraftプラグイン)でだいたい半々ぐらいの割合だったのですが、コードが肥大化するにつれてここって何が返ってくるんだっけ?とかここの引数ってなんだったっけ?という事が発生しました。静的型付けって偉大なんだな...
最初は手探りでコードを書いていたのでここまで肥大化するのは想定できていませんでしたが、今後また個人でシステム開発をする際は最初からTypeScriptで書こうかなと思った次第です。
最後に
最後までご覧いただきありがとうございました!こういった技術記事を書くのは初めてなのでお見苦しかったかもしれませんが...
実はこのシステム、発想から完成させるまでに約一年かかってしまいました。どのように実装したものかと悩みながらコツコツコードを積み重ねてみたり、途中でphpを書く必要が発生して一から勉強したりと大変でしたが、開発を通してできることを少しだけ増やすことができたかなと感じます。
それでは。
github: https://github.com/mam1zu
twitter: https://twitter.com/_Mamizu
Discussion