之前看麦块科技的外置登录,是通过修改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(); } } }
2020.2.3考古