Skip to content

认证接入实战:源码与 Velocity ​

认证接入篇讲了流程和配置。这篇深入源码——把 net_bc.diff 的关键改动拆给你看,并补充 Velocity 代理的接入思路。

认证流程总览 ​

原版 BungeeCord 玩家登录时,BC 用 GET 请求 Mojang 的 sessionserver.mojang.com/session/minecraft/hasJoined 验证。中国版改成:

玩家登录 → BC 收到 EncryptionResponse
        → 构造 HasJoinedRequest(username, serverId, gameId)
        → POST 到 auth_url(网易认证服)
        → 返回 LoginResult(id, name)
        → 验证通过则放行,失败则踢出

核心就三处改动

  1. GET → POST,body 是 JSON
  2. 认证地址从 Mojang 改成网易 auth_url
  3. 请求体带上 gameId 标识你的游戏

源码实例(基于 net_bc.diff) ​

1. Configuration.java —— 新增两个配置项 ​

读取 config.yml 里的 game_id 和 auth_url:

java
@Getter
public class Configuration implements ProxyConfig {
    // ... 原有字段 ...

    private String gameId;                          // 新增:游戏 iid
    private String url = "http://localhost:9999/check"; // 新增:认证服地址

    public void load() {
        ConfigurationAdapter adapter = ProxyServer.getInstance().getConfigurationAdapter();
        adapter.load();
        // ... 原有读取 ...
        gameId = adapter.getString("game_id", "");        // 读 game_id
        url = adapter.getString("auth_url", url);         // 读 auth_url
        // ...
    }
}

@Getter(Lombok)自动生成 getGameId() 和 getUrl()。

2. HttpClient.java —— 新增 POST 支持 ​

原版只有 get,中国版加了 post 方法,支持发 JSON body:

java
public static void post(String url, Object data, EventLoop eventLoop, Callback<String> callback) {
    httpRequest(url, data, eventLoop, callback);
}

private static void httpRequest(String url, final Object data, EventLoop eventLoop,
                                final Callback<String> callback) {
    // ... 解析 URI、端口、DNS 缓存(同 get)...

    ChannelFutureListener future = new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            if (future.isSuccess()) {
                String path = uri.getRawPath()
                    + ((uri.getRawQuery() == null) ? "" : "?" + uri.getRawQuery());

                HttpRequest request;
                if (data == null) {
                    // 无 body → GET
                    request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
                } else {
                    // 有 body → POST,Content-Type: application/json
                    DefaultFullHttpRequest fullRequest =
                        new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, path);
                    fullRequest.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json");
                    String content = BungeeCord.getInstance().gson.toJson(data); // 序列化
                    byte[] raw = content.getBytes("UTF-8");
                    fullRequest.headers().set(HttpHeaders.Names.CONTENT_LENGTH, raw.length);
                    fullRequest.content().clear().writeBytes(raw);
                    request = fullRequest;
                }
                request.headers().set(HttpHeaders.Names.HOST, uri.getHost());
                future.channel().writeAndFlush(request);
            } else {
                addressCache.invalidate(uri.getHost());
                callback.done(null, future.cause());
            }
        }
    };
    // ... 建连 ...
}

关键点

  • 用 DefaultFullHttpRequest(带 body)而非 DefaultHttpRequest(无 body)
  • Content-Type: application/json,body 用 Gson 序列化 data 对象
  • data == null 时退回 GET,兼容原行为

3. HasJoinedRequest.java —— 请求体结构 ​

表示发给认证服的参数(构造时传入 username、serverId、gameId):

java
public class HasJoinedRequest {
    private String username;
    private String serverId;
    private String gameId;

    public HasJoinedRequest(String username, String serverId, String gameId) {
        this.username = username;
        this.serverId = serverId;
        this.gameId = gameId;
    }
    // getter ...
}

Gson 序列化后 JSON 形如:

json
{
  "username": "Steve",
  "serverId": "abc123...",
  "gameId": "<你的游戏 gameid>"
}

4. InitialHandler.java —— 认证调用 ​

handle(EncryptionResponse) 里把原版 GET Mojang 改成 POST 网易认证服:

java
@Override
public void handle(final EncryptionResponse encryptResponse) throws Exception {
    // ... 解密握手、算 serverId ...

    String serverID = new BigInteger(sha.digest()).toString(16);

    // 构造请求体:用户名 + serverId + gameId
    final HasJoinedRequest data =
        new HasJoinedRequest(username, serverID, this.getGameId());

    Callback<String> handler = new Callback<String>() {
        @Override
        public void done(String result, Throwable error) {
            if (error == null) {
                // 解析响应
                LoginResult obj = BungeeCord.getInstance().gson.fromJson(result, LoginResult.class);
                if (obj != null && obj.getId() != null) {
                    loginProfile = obj;
                    name = obj.getName();
                    // ... 完成登录 ...
                } else {
                    disconnect(bungee.getTranslation("offline_mode_player"));
                }
            } else {
                disconnect(bungee.getTranslation("mojang_fail"));
                bungee.getLogger().log(Level.SEVERE,
                    "Error authenticating " + getName() + " with minecraft.net", error);
            }
        }
    };

    // 原版:HttpClient.get(authURL, ch.getHandle().eventLoop(), handler);
    // 改后:POST 到 auth_url
    HttpClient.post(this.getUrl(), data, ch.getHandle().eventLoop(), handler);
}

响应结构 LoginResult

网易认证服返回的(玩家 UUID)和 name(玩家名)。obj.getId() != null 即认证通过,取 id/name 完成登录。

BungeeCord 接入小结 ​

文件改动
Configuration.java新增 gameId、url 字段,load() 读 game_id/auth_url
HttpClient.java新增 post() 方法,支持 JSON body
HasJoinedRequest.java新增类,表示请求体结构
InitialHandler.java认证调用从 get(Mojang) 改 post(网易 auth_url)

完整改动见官方 net_bc.diff,每行都在里面。

Velocity 接入 ​

官方接受 Velocity

中国版提供的认证 patch(net_bc.diff)针对 BungeeCord 源码,官方未提供 Velocity 现成方案。但 Velocity 同样被接受——关键不在于用 BC 还是 Velocity,而在于认证接好、能通过中国版客户端进入服务器。BC 用现成 patch 省事,Velocity 需参照以下思路自行实现认证插件。

思路 ​

Velocity 的认证逻辑用插件实现,而非改源码:监听玩家预登录事件,异步 POST 到 auth_url 验证,通过则放行、失败则踢出。

BC 改源码Velocity 插件
改 InitialHandler监听 PreLoginEvent
改 HttpClient用 Java 标准库 HttpClient
config.yml 读 auth_url/game_id插件自定义配置读
编译进 BC jar独立插件 jar

参考插件骨架 ​

java
@Plugin(id = "netease-auth", name = "NetEaseAuth", version = "1.0")
public class NetEaseAuthPlugin {

    private ProxyServer server;
    private String authUrl;
    private String gameId;
    private final HttpClient httpClient = HttpClient.newHttpClient();
    private final Gson gson = new Gson();

    @Subscribe
    public void onProxyInit(ProxyInitializeEvent event) {
        // 从插件 config 读 auth_url、game_id
        authUrl = loadConfig("auth_url");
        gameId = loadConfig("game_id");
    }

    @Subscribe(order = PostOrder.FIRST)
    public void onPreLogin(PreLoginEvent event) {
        String username = event.getUsername();

        // PreLoginEvent 同步,网络请求必须异步 + 阻塞等待结果
        event.setResult(PreLoginEvent.PreLoginComponentResult.denied(
            Component.text("正在验证..."))); // 先占位

        // 异步验证
        server.getScheduler().buildTask(this, () -> {
            try {
                // 构造请求体(同 HasJoinedRequest)
                Map<String, String> body = new HashMap<>();
                body.put("username", username);
                body.put("serverId", computeServerId(event)); // 按 MC 协议算
                body.put("gameId", gameId);

                HttpRequest req = HttpRequest.newBuilder()
                    .uri(URI.create(authUrl))
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body)))
                    .timeout(Duration.ofSeconds(5))
                    .build();

                HttpResponse<String> resp = httpClient.send(req,
                    HttpResponse.BodyHandlers.ofString());

                // 解析响应(同 LoginResult)
                JsonObject obj = gson.fromJson(resp.body(), JsonObject.class);
                if (obj != null && obj.get("id") != null) {
                    // 验证通过 —— 注意:PreLoginEvent 已结束,
                    // 实际需用 LoginPhaseEvent 或重连机制,见下方说明
                } else {
                    // 踢出
                }
            } catch (Exception e) {
                // 认证失败处理
            }
        }).schedule();
    }
}

这只是骨架,不能直接用

PreLoginEvent 是同步的,事件返回时异步认证还没完成。真正落地需用 Velocity 的 LoginPhaseEvent(1.19.4+)或连接状态机,在认证回调里决定放行/踢出。serverId 的算法、响应字段也需对照 BC 的 InitialHandler 和实际抓包确认。建议直接用官方 BC 方案,Velocity 方案仅作技术探索。

BC vs Velocity 取舍 ​

维度BungeeCordVelocity
官方认证支持✅ 有 patch + 源码包❌ 无,需自行实现
上线合规接受同样接受(认证接好、能通过中国版客户端进入即可)
性能够用更好
接入成本低(用现成的)高(自己写+调试+验证)

按团队技术栈选

BC 和 Velocity 都被接受。BC 有现成认证 patch,接入省事;Velocity 性能更好,但需自行实现认证插件。核心验收标准统一:认证接好、能通过中国版客户端进入服务器。按团队熟悉的技术栈选即可。


源码实例和 Velocity 内容到此。认证接好后,上架前还有最后一关——二审(内容审核)。回到认证接入或Java 网络游戏首页。