本文内容较新 · 今天更新
最后更新: 2026年02月04日
预计阅读时间: 39.0 分钟
9739 字 250 字/分

今年准备开服的时候,想继续沿用CaaMoe的MultiLogin混合登录方案,结果意外的发现他们的混合登录方案已经放弃对Bukkit的支持了,正好一直在学习SpigotAPI的开发,于是乎决定VibeCoding自己写一套混合登录方案

作为混合登录,有一个需要处理的点是,服务器经常不仅需要支持正版,还需要支持各种外置皮肤站Yggdrasil的登陆验证,这导致我们可能需要使用非常多的if-else判断语句,导致代码可读性和可维护性都很差,甚至导致玩家的登陆速度奇慢

所以我想到了一个责任链的方案,引入责任链模式 (Chain of  Responsibility) ,优化认证逻辑

我在我的XiMultiLogin中定义了一个AuthProvider接口,这样每个认证源只需要实现自己的接口即可

/**
 * 具体的 Mojang 验证节点
 */
public class MojangAuthProvider implements AuthProvider {
    private final Object originalSessionService;
    private final boolean enabled;


    public MojangAuthProvider(Object originalSessionService, boolean enabled) {
        this.originalSessionService = originalSessionService;
        this.enabled = enabled;
    }


    @Override
    public Object authenticate(String username, String serverId) {
        // 调用 Minecraft 源码中的 originalSessionService 进行正版验证
        // 这里通常涉及反射或直接调用底层映射的方法
        try {
            return ReflectionUtils.invoke(originalSessionService, "hasJoinedServer", username, serverId);
        } catch (Exception e) {
            return null; // 验证失败或网络波动返回 null,交给链条下一个节点
        }
    }


    @Override public String getName() { return "MOJANG"; }
    @Override public boolean isEnabled() { return enabled; }
}


/**
 * 灵活的 Yggdrasil 验证节点(支持外部 API)
 */
public class YggdrasilAuthProvider implements AuthProvider {
    private final String name;
    private final String apiUrl;
    private final boolean enabled;


    public YggdrasilAuthProvider(String name, String apiUrl, boolean enabled) {
        this.name = name;
        this.apiUrl = apiUrl;
        this.enabled = enabled;
    }


    @Override
    public Object authenticate(String username, String serverId) {
        // 伪代码:构造请求发送至第三方的验证服务器(如 LittleSkin / Blessing Skin)
        // HttpClient.post(apiUrl + "/hasJoined", {user: username, id: serverId})
        // 这种设计允许我们在不重启插件的情况下,只通过配置就接入无数个皮肤站
        return YggdrasilClient.verify(this.apiUrl, username, serverId);
    }


    @Override public String getName() { return name; }
    @Override public boolean isEnabled() { return enabled; }
}

但传统的责任链有个问题,即他是逐个攻破,从第一个开始,第一个无法解决再交给第二个,这样会极大的影响玩家的登入效率,所以我采用了竞态并行的方法,让接口同时开始处理登录事件

/**

 * 异步并行调度核心
 */
private CompletableFuture<Object> tryAllProvidersAsync(String username, String serverId) {
    // 1. 转换任务流
    List<CompletableFuture<Object>> taskList = providers.stream()
            .map(p -> CompletableFuture.supplyAsync(() -> {
                try {
                    return p.authenticate(username, serverId);
                } catch (Exception e) {
                    return null; 
                }
            }, AUTH_EXECUTOR))
            .collect(Collectors.toList());

    // 2. 等待聚合结果
    return CompletableFuture.allOf(taskList.toArray(new CompletableFuture[0]))
            .thenApply(v -> {
                // 3. 这里的逻辑类似并行的“短路”:找到第一个不为 null 的成功结果
                for (CompletableFuture<Object> future : taskList) {
                    Object result = future.getNow(null);
                    if (result != null) return result;
                }
                return null;
            })
            .thenCompose(finalResult -> {
                // 4. 降级逻辑:如果全部失败且允许盗版
                if (finalResult == null && configManager.isAllowCracked()) {
                    return CompletableFuture.completedFuture(createTemporaryProfile(username));
                }
                return CompletableFuture.completedFuture(finalResult);
            });
}

如果在传统的责任链模式中,考虑到Mojang的神奇验证服务器经常挂掉,而Mojang验证又作为插件的第一个验证方式,极可能导致登陆验证长到离谱,用并行的方式可以迅速的知道哪些方式是可用的,响应速度取决于最快的节点,而不会被最慢的节点拖累

当然,我们还需要使用配置文件来动态调整所有的登录模式

/**
 * 在 XiSessionService 中初始化责任链
 */
private void initializeProviders(ConfigManager configManager) {
    List<ConfigManager.ProviderConfig> configs = configManager.getPipelineConfig();
    
    for (ConfigManager.ProviderConfig cfg : configs) {
        if (!cfg.isEnabled()) continue;

        AuthProvider provider = null;
        // 使用简单的工厂逻辑创建节点
        switch (cfg.getType().toUpperCase()) {
            case "MOJANG":
                provider = new MojangAuthProvider(originalSessionService, true);
                break;
            case "YGGDRASIL":
                provider = new YggdrasilAuthProvider(cfg.getName(), cfg.getApiUrl(), true);
                break;
        }

        if (provider != null) {
            this.providers.add(provider);
            LOGGER.info("已加载认证节点: " + provider.getName());
        }
    }
}

在分布式系统中,验证延迟 $L$ 往往是整个链路的瓶颈。通过将串行 $L = \sum l_i$ 优化为并行 $L = \min(l_1, l_2, \dots, l_n)$,我们不仅提升了速度,更增强了系统的容错能力。 责任链模式与 Java 异步并发的结合,为 XiMultiLogin 提供了高度的灵活性。目前插件已能完美胜任多验证源混合登录的需求。代码已同步至我的 GitHub XiNian-dada/XiMultiLogin