利用Socket配合Java插件实现玩家外置登录

之前看麦块科技的外置登录,是通过修改Mojang的验证服务器地址实现的,这种方法很安全,但是我们其实也可以通过另外一种方式实现外置登录。

有一个项目叫BeeLogin,其原理是比较玩家数据实现外置登录(大概是这样)

但是这种方法可能会存在以下问题:

1.如果服务器是通过FRP/Ngrok/花生壳等映射工具映射的,那么玩家IP地址将会全部变成127.0.0.1,这种情况下很难判断用户是否已经登录。

2.如果玩家登录了又不进入游戏,其他人可能可以登录该玩家的账号(同一局域网下,同IP)

所以,我们可以从另一个角度来想方法,如果认证本身不在玩家身上呢?

于是我就开发了这个外置登录系统。

其原理很简单,玩家在客户端启动器中登录,登录后启动器最小化隐藏在后台,并且与服务器建立socket,然后每隔2秒向服务器发送玩家输入的用户名和密码,服务器接收到后,判断玩家是否在游戏中,如果不在线,则略过,启动器继续发送用户名和密码;如果玩家在线,则判断用户名和密码是否正确,如果正确,则允许玩家进行游戏,然后向启动器回复exit,启动器接收到后退出。

这种方法的好处是,不需要像麦块科技那样开启正版验证,且这种外置登录非常灵活,可以自己根据需要进行修改。

下面给出一个简单的demo,演示外置登录是如何通讯的。

package cn.kasuganosora.login;

import static cn.kasuganosora.login.Main.accountStatus;
import static cn.kasuganosora.login.Main.color_decode;
import static cn.kasuganosora.login.Main.loginStatus;
import static cn.kasuganosora.login.Main.prefix;
import static cn.kasuganosora.login.Main.textColor;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;
import java.util.logging.Level;
import static org.bukkit.Bukkit.getLogger;
import static org.bukkit.Bukkit.getServer;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffectType;

public class SocketServer {

    static class Task implements Runnable {

        private final Socket socket;

        public Task(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                // 创建线程,启动 Socket
                handlerSocket();
            } catch (Exception e) {
                getLogger().log(Level.WARNING, "\u521b\u5efa\u7ebf\u7a0b\u65f6\u51fa\u73b0\u5f02\u5e38\uff1a{0}", e.getLocalizedMessage());
            }
        }

        private void handlerSocket() throws Exception {

            try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"))) {
                StringBuilder sb = new StringBuilder();
                String temp;
                int index;
                while ((temp = br.readLine()) != null) {
                    if ((index = temp.indexOf("eof")) != -1) {
                        sb.append(temp.substring(0, index));
                        break;
                    }
                    sb.append(temp);
                }
                // 获取客户端启动器发送来的内容
                String brind = sb.toString();
                // 客户端发送的数据格式为 用户名/密码,因此用 / 将数据分割并存到数组
                String[] msgtag = brind.split("/");
                // 如果数组大小 > 1则正确
                if (msgtag.length > 1) {
                    // 尝试获取这个玩家,如果该玩家不在线则会返回 null
                    if (getServer().getPlayer(msgtag[0]) != null) {
                        // 获取玩家对象
                        Player player = getServer().getPlayer(msgtag[0]);
                        // 从 loginStatus 这个 Map 里取玩家登录状态
                        if (loginStatus.get(player.getUniqueId().toString()) == false) {
                            // 奇葩的判断密码错误次数方法,如果 accountStatus 的值等于五个 / 就等于密码输入错误 5 次
                            if ("/////".equals(accountStatus.get(player.getUniqueId().toString()))) {
                                // 密码错误就说明这不是官方启动器咯
                                player.kickPlayer(textColor + "您未通过官方启动器启动的游戏。");
                                try (Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8")) {
                                    // 尝试通知启动器结束进程
                                    writer.write("exit");
                                    writer.flush();
                                }
                                return;
                            }
                            // 密码、账号都没问题,尝试登陆,给玩家发送一个正在登陆的提示
                            player.sendMessage(prefix + textColor + "正在登录...");
                            // 从数组 1 获取密码
                            String Password = msgtag[1];
                            // 获取不含颜色代码的玩家名
                            String nameWithoutColorCode = color_decode(player.getName());
                            // 启动新线程,主线程请求 HTTP 会导致服务器卡顿
                            new Thread() {
                                @Override
                                public void run() {
                                    // 调用 HTTP 请求类验证账号密码
                                    String returnCode = Http.LoadHTTP("http://localhost/niconicocraft/kasuganosora/server/login/?user=" + nameWithoutColorCode + "&pass=" + Password, "");
                                    // 如果返回数据等于文本的“200”
                                    if (returnCode.equals("200")) {
                                        // 设置 loginStatus 这个 Map 里玩家登录状态为 true
                                        loginStatus.replace(player.getUniqueId().toString(), true);
                                        // 提示玩家登陆成功,并执行登陆后的操作
                                        player.sendMessage(prefix + textColor + "账号登录成功!");
                                        player.loadData();
                                        player.setWalkSpeed(0.2f);
                                        player.removePotionEffect(PotionEffectType.BLINDNESS);
                                        player.removePotionEffect(PotionEffectType.DAMAGE_RESISTANCE);
                                        Main.checkGoods(player);
                                        try (Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8")) {
                                            // 通知启动器退出
                                            writer.write("exit");
                                            writer.flush();
                                        } catch (IOException ex) {
                                            // 此处异常是因为 Socket 断开,通常是启动器已经结束,因此异常可以直接无视
                                        }
                                    } else {
                                        // 如果返回状态不等于“200”,说明玩家以特殊的方式绕开了登录验证
                                        player.sendMessage(prefix + textColor + "您不是通过官方启动器启动的游戏。");
                                        player.sendMessage(prefix + textColor + "请直接在聊天栏中输入密码登录。");
                                        accountStatus.replace(player.getUniqueId().toString(), accountStatus.get(player.getUniqueId().toString()) + "/");
                                        try (Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8")) {
                                            // 尝试通知启动器结束进程
                                            writer.write("exit");
                                            writer.flush();
                                        } catch (IOException ex) {
                                            // Socket 已关闭,原因同上
                                        }
                                    }
                                }
                            }.start();
                        } else {
                            try (Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8")) {
                                // 如果玩家已经登陆了,直接通知启动器结束
                                writer.write("exit");
                                writer.flush();
                            }
                        }
                    } else {
                        try (Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8")) {
                            // 玩家不在线,略过启动器请求,但是还是返回一句 offline 以表敬意...
                            writer.write("offline");
                            writer.flush();
                        }
                    }
                } else {
                    try (Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8")) {
                        // 发送的内容不是以 用户名/密码 格式的,说明有问题,返回Bad Request
                        writer.write("Bad Request");
                        writer.flush();
                    }
                }
            }
            // 一切操作完成,关闭Socket
            socket.close();
        }
    }
}

推荐阅读文章

1 条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注