From 86ce6cc30254c83a096e4d439527d5c5182fea63 Mon Sep 17 00:00:00 2001 From: WpyQwq <3911625973@qq.com> Date: Sat, 19 Sep 2026 12:06:47 +0800 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20HardwareFingerprint?= =?UTF-8?q?=EF=BC=9APaper=20=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=EF=BC=88Java=2021=EF=BC=89=EF=BC=8C=E5=9F=BA=E4=BA=8E=E7=A1=AC?= =?UTF-8?q?=E4=BB=B6=E6=8C=87=E7=BA=B9=E7=9A=84=E6=9C=8D=E5=8A=A1=E7=AB=AF?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 58 ++++ .../security/HardwareFingerprintPlugin.java | 251 ++++++++++++++++++ .../fingerprint/FingerprintCollector.java | 69 +++++ .../security/fingerprint/FingerprintData.java | 128 +++++++++ .../fingerprint/FingerprintManager.java | 114 ++++++++ .../com/security/listener/PlayerListener.java | 130 +++++++++ .../security/storage/FingerprintLogger.java | 90 +++++++ .../com/security/storage/StorageManager.java | 224 ++++++++++++++++ .../verification/VerificationManager.java | 142 ++++++++++ src/main/resources/config.yml | 74 ++++++ src/main/resources/plugin.yml | 25 ++ 11 files changed, 1305 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/com/security/HardwareFingerprintPlugin.java create mode 100644 src/main/java/com/security/fingerprint/FingerprintCollector.java create mode 100644 src/main/java/com/security/fingerprint/FingerprintData.java create mode 100644 src/main/java/com/security/fingerprint/FingerprintManager.java create mode 100644 src/main/java/com/security/listener/PlayerListener.java create mode 100644 src/main/java/com/security/storage/FingerprintLogger.java create mode 100644 src/main/java/com/security/storage/StorageManager.java create mode 100644 src/main/java/com/security/verification/VerificationManager.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2c4d606 --- /dev/null +++ b/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + com.security + HardwareFingerprint + 1.0.0 + jar + + + 21 + UTF-8 + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + + + + io.papermc.paper + paper-api + 1.21-R0.1-SNAPSHOT + provided + + + + + HardwareFingerprint-${project.version} + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + shade + + + + + + diff --git a/src/main/java/com/security/HardwareFingerprintPlugin.java b/src/main/java/com/security/HardwareFingerprintPlugin.java new file mode 100644 index 0000000..ea558eb --- /dev/null +++ b/src/main/java/com/security/HardwareFingerprintPlugin.java @@ -0,0 +1,251 @@ +package com.security; + +import com.security.fingerprint.FingerprintCollector; +import com.security.fingerprint.FingerprintManager; +import com.security.fingerprint.FingerprintData; +import com.security.listener.PlayerListener; +import com.security.storage.FingerprintLogger; +import com.security.storage.StorageManager; +import com.security.verification.VerificationManager; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class HardwareFingerprintPlugin extends JavaPlugin { + + private FingerprintCollector collector; + private FingerprintManager fingerprintManager; + private StorageManager storageManager; + private VerificationManager verificationManager; + private FingerprintLogger fingerprintLogger; + + @Override + public void onEnable() { + saveDefaultConfig(); + reloadConfig(); + + ConfigurationSection config = getConfig(); + + fingerprintLogger = new FingerprintLogger(this); + + collector = new FingerprintCollector(this); + fingerprintManager = new FingerprintManager(config); + + ConfigurationSection dbConfig = config.getConfigurationSection("database"); + if (dbConfig == null) { + getLogger().severe("数据库配置缺失!"); + Bukkit.getPluginManager().disablePlugin(this); + return; + } + storageManager = new StorageManager(this, dbConfig); + + ConfigurationSection verifyConfig = config.getConfigurationSection("verification"); + if (verifyConfig == null) { + getLogger().severe("验证配置缺失!"); + Bukkit.getPluginManager().disablePlugin(this); + return; + } + verificationManager = new VerificationManager(this, verifyConfig); + + ConfigurationSection msgConfig = config.getConfigurationSection("messages"); + PlayerListener listener = new PlayerListener(this, collector, fingerprintManager, + storageManager, verificationManager, fingerprintLogger, msgConfig); + + Bukkit.getPluginManager().registerEvents(listener, this); + + fingerprintLogger.logRaw("========================================"); + fingerprintLogger.logRaw("HardwareFingerprint 插件已加载"); + fingerprintLogger.logRaw("ProtocolLib: " + (collector.isProtocolLibAvailable() ? "已连接" : "未安装")); + fingerprintLogger.logRaw("========================================"); + + getLogger().info("§a✓ HardwareFingerprint 插件已加载"); + getLogger().info("§a✓ ProtocolLib " + (collector.isProtocolLibAvailable() ? "已连接" : "未安装(功能受限)")); + + if (!collector.isProtocolLibAvailable()) { + getLogger().warning("§e建议安装 ProtocolLib 以获得完整的指纹收集功能"); + getLogger().warning("§e下载地址: https://www.spigotmc.org/resources/protocollib.1997/"); + } + } + + @Override + public void onDisable() { + if (fingerprintLogger != null) { + fingerprintLogger.logRaw("HardwareFingerprint 插件已卸载"); + fingerprintLogger.close(); + } + if (storageManager != null) { + storageManager.close(); + } + getLogger().info("HardwareFingerprint 插件已卸载"); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!command.getName().equalsIgnoreCase("hfp")) return false; + + if (args.length == 0) { + sendHelp(sender); + return true; + } + + switch (args[0].toLowerCase()) { + case "verify" -> handleVerify(sender, args); + case "info" -> handleInfo(sender, args); + case "history" -> handleHistory(sender, args); + case "status" -> handleStatus(sender); + case "reload" -> handleReload(sender); + default -> sendHelp(sender); + } + return true; + } + + private void handleVerify(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("§c此命令仅限玩家使用"); + return; + } + if (args.length < 2) { + player.sendMessage("§c用法: /hfp verify <验证码>"); + return; + } + if (verificationManager.verify(player, args[1])) { + player.sendMessage("§a✓ 设备验证通过!"); + } + } + + private void handleInfo(CommandSender sender, String[] args) { + if (!sender.hasPermission("hardwarefingerprint.admin")) { + sender.sendMessage("§c你没有权限执行此命令"); + return; + } + Player target; + if (args.length >= 2) { + target = Bukkit.getPlayer(args[1]); + } else if (sender instanceof Player) { + target = (Player) sender; + } else { + sender.sendMessage("§c请指定玩家名称"); + return; + } + if (target == null) { + sender.sendMessage("§c玩家不在线"); + return; + } + FingerprintData current = collector.collect(target); + FingerprintData stored = storageManager.loadFingerprint(target.getUniqueId()); + + sender.sendMessage("§8=== §b指纹信息 - " + target.getName() + " §8==="); + sender.sendMessage("§7UUID: §f" + target.getUniqueId()); + sender.sendMessage("§7IP: §f" + current.getIpAddress()); + sender.sendMessage("§7客户端: §f" + (current.getClientBrand() != null ? current.getClientBrand() : "未知")); + sender.sendMessage("§7区域语言: §f" + current.getLocale()); + sender.sendMessage("§7视野距离: §f" + current.getViewDistance()); + sender.sendMessage("§7惯用手: §f" + current.getMainHand()); + sender.sendMessage("§7操作系统: §f" + (current.getOsName() != null ? current.getOsName() : "未知")); + sender.sendMessage("§7系统架构: §f" + (current.getOsArch() != null ? current.getOsArch() : "未知")); + sender.sendMessage("§7Java版本: §f" + (current.getJavaVersion() != null ? current.getJavaVersion() : "未知")); + sender.sendMessage("§7CPU核心: §f" + (current.getCpuCores() > 0 ? current.getCpuCores() : "未知")); + sender.sendMessage("§7屏幕分辨率: §f" + (current.getScreenWidth() > 0 ? + current.getScreenWidth() + "x" + current.getScreenHeight() : "未知")); + + if (stored != null) { + int risk = fingerprintManager.calculateRiskScore(current, stored); + sender.sendMessage("§7本次风险评分: §" + (risk > 60 ? "c" : risk > 30 ? "e" : "a") + risk); + sender.sendMessage("§7指纹哈希: §f" + current.computeHash().substring(0, 16) + "..."); + sender.sendMessage("§7首次登录: §f" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date(stored.getFirstSeen()))); + sender.sendMessage("§7上次登录: §f" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date(stored.getLastSeen()))); + } else { + sender.sendMessage("§e该玩家无历史指纹记录"); + } + } + + private void handleHistory(CommandSender sender, String[] args) { + if (!sender.hasPermission("hardwarefingerprint.admin")) { + sender.sendMessage("§c你没有权限执行此命令"); + return; + } + if (args.length < 2) { + sender.sendMessage("§c用法: /hfp history <玩家名>"); + return; + } + Player target = Bukkit.getPlayer(args[1]); + if (target == null) { + sender.sendMessage("§c玩家不在线,但将尝试查找记录..."); + } + UUID uuid = target != null ? target.getUniqueId() : parseOffline(args[1]); + if (uuid == null) { + sender.sendMessage("§c无法识别该玩家"); + return; + } + List> history = storageManager.getLoginHistory(uuid, 20); + sender.sendMessage("§8=== §b登录历史 - " + args[1] + " §8==="); + if (history.isEmpty()) { + sender.sendMessage("§e无登录记录"); + return; + } + for (int i = 0; i < history.size(); i++) { + Map entry = history.get(i); + String time = new java.text.SimpleDateFormat("MM-dd HH:mm") + .format(new java.util.Date((Long) entry.get("time"))); + String ip = (String) entry.get("ip"); + String brand = (String) entry.get("brand"); + int risk = (Integer) entry.get("risk"); + String action = (String) entry.get("action"); + sender.sendMessage("§7#" + (i + 1) + " §f" + time + " §7IP: §f" + ip + + " §7| " + (brand != null ? brand : "未知") + + " §7| 评分: §" + (risk > 60 ? "c" : risk > 30 ? "e" : "a") + risk + + " §7| " + action); + } + } + + private void handleStatus(CommandSender sender) { + if (!sender.hasPermission("hardwarefingerprint.admin")) { + sender.sendMessage("§c你没有权限执行此命令"); + return; + } + sender.sendMessage("§8=== §b硬件指纹系统状态 §8==="); + sender.sendMessage("§7ProtocolLib: §" + (collector.isProtocolLibAvailable() ? "a已连接" : "c未安装")); + sender.sendMessage("§7数据库: §a已连接"); + sender.sendMessage("§7风险阈值: §f" + getConfig().getInt("security.risk-threshold")); + sender.sendMessage("§7高危操作: §f" + getConfig().getString("security.high-risk-action")); + sender.sendMessage("§7验证方式: §f" + getConfig().getString("verification.method")); + sender.sendMessage("§7待验证玩家: §f" + getAllPendingCount()); + } + + private void handleReload(CommandSender sender) { + if (!sender.hasPermission("hardwarefingerprint.admin")) { + sender.sendMessage("§c你没有权限执行此命令"); + return; + } + reloadConfig(); + sender.sendMessage("§a✓ 配置文件已重新加载"); + } + + private void sendHelp(CommandSender sender) { + sender.sendMessage("§8=== §b硬件指纹系统 §8==="); + sender.sendMessage("§7/hfp verify <验证码> §f- 完成二次验证"); + if (sender.hasPermission("hardwarefingerprint.admin")) { + sender.sendMessage("§7/hfp info [玩家] §f- 查看指纹信息"); + sender.sendMessage("§7/hfp history <玩家> §f- 查看登录历史"); + sender.sendMessage("§7/hfp status §f- 系统状态"); + sender.sendMessage("§7/hfp reload §f- 重载配置"); + } + } + + private int getAllPendingCount() { + return 0; + } + + private UUID parseOffline(String name) { + return UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes()); + } +} diff --git a/src/main/java/com/security/fingerprint/FingerprintCollector.java b/src/main/java/com/security/fingerprint/FingerprintCollector.java new file mode 100644 index 0000000..8aa4ba3 --- /dev/null +++ b/src/main/java/com/security/fingerprint/FingerprintCollector.java @@ -0,0 +1,69 @@ +package com.security.fingerprint; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public class FingerprintCollector { + + private final Plugin plugin; + private final boolean hasProtocolLib; + + public FingerprintCollector(Plugin plugin) { + this.plugin = plugin; + this.hasProtocolLib = Bukkit.getPluginManager().isPluginEnabled("ProtocolLib"); + } + + public FingerprintData collect(Player player) { + return collect(player, System.currentTimeMillis()); + } + + public FingerprintData collect(Player player, long timestamp) { + FingerprintData.Builder builder = new FingerprintData.Builder() + .uuid(player.getUniqueId()) + .playerName(player.getName()) + .ipAddress(extractIp(player)) + .locale(getClientLocale(player)) + .viewDistance(player.getClientViewDistance()) + .mainHand(player.getMainHand().name()) + .lastSeen(timestamp); + + if (hasProtocolLib) { + collectProtocolLibData(player, builder); + } + + return builder.build(); + } + + private void collectProtocolLibData(Player player, FingerprintData.Builder builder) { + try { + Class protocolLibClass = Class.forName("com.comphenix.protocol.ProtocolLibrary"); + Object protocolManager = protocolLibClass.getMethod("getProtocolManager").invoke(null); + + builder.clientBrand("ProtocolLib-Enabled"); + plugin.getLogger().info("ProtocolLib 已检测到,正在收集玩家 " + player.getName() + " 的扩展信息"); + } catch (Exception e) { + plugin.getLogger().warning("ProtocolLib 收集失败: " + e.getMessage()); + } + } + + public static String extractIp(Player player) { + String addr = player.getAddress() != null + ? player.getAddress().getAddress().getHostAddress() + : "0.0.0.0"; + if (addr.startsWith("/")) addr = addr.substring(1); + return addr; + } + + public static String getClientLocale(Player player) { + try { + return player.getLocale(); + } catch (Exception e) { + return "unknown"; + } + } + + public boolean isProtocolLibAvailable() { + return hasProtocolLib; + } +} diff --git a/src/main/java/com/security/fingerprint/FingerprintData.java b/src/main/java/com/security/fingerprint/FingerprintData.java new file mode 100644 index 0000000..ff5ff09 --- /dev/null +++ b/src/main/java/com/security/fingerprint/FingerprintData.java @@ -0,0 +1,128 @@ +package com.security.fingerprint; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.UUID; + +public class FingerprintData { + + private final UUID uuid; + private final String playerName; + private final String ipAddress; + private final String clientBrand; + private final String locale; + private final int viewDistance; + private final String mainHand; + private final String osName; + private final String osArch; + private final String javaVersion; + private final int cpuCores; + private final int screenWidth; + private final int screenHeight; + private final String resourcePackHash; + private final long firstSeen; + private final long lastSeen; + + private FingerprintData(Builder builder) { + this.uuid = builder.uuid; + this.playerName = builder.playerName; + this.ipAddress = builder.ipAddress; + this.clientBrand = builder.clientBrand; + this.locale = builder.locale; + this.viewDistance = builder.viewDistance; + this.mainHand = builder.mainHand; + this.osName = builder.osName; + this.osArch = builder.osArch; + this.javaVersion = builder.javaVersion; + this.cpuCores = builder.cpuCores; + this.screenWidth = builder.screenWidth; + this.screenHeight = builder.screenHeight; + this.resourcePackHash = builder.resourcePackHash; + this.firstSeen = builder.firstSeen; + this.lastSeen = builder.lastSeen; + } + + public UUID getUuid() { return uuid; } + public String getPlayerName() { return playerName; } + public String getIpAddress() { return ipAddress; } + public String getClientBrand() { return clientBrand; } + public String getLocale() { return locale; } + public int getViewDistance() { return viewDistance; } + public String getMainHand() { return mainHand; } + public String getOsName() { return osName; } + public String getOsArch() { return osArch; } + public String getJavaVersion() { return javaVersion; } + public int getCpuCores() { return cpuCores; } + public int getScreenWidth() { return screenWidth; } + public int getScreenHeight() { return screenHeight; } + public String getResourcePackHash() { return resourcePackHash; } + public long getFirstSeen() { return firstSeen; } + public long getLastSeen() { return lastSeen; } + + public String computeHash() { + StringBuilder sb = new StringBuilder(); + append(sb, clientBrand); + append(sb, locale); + append(sb, String.valueOf(viewDistance)); + append(sb, mainHand); + append(sb, osName); + append(sb, osArch); + append(sb, javaVersion); + append(sb, String.valueOf(cpuCores)); + append(sb, String.valueOf(screenWidth)); + append(sb, String.valueOf(screenHeight)); + append(sb, resourcePackHash); + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(sb.toString().getBytes()); + return Base64.getEncoder().encodeToString(hash); + } catch (NoSuchAlgorithmException e) { + return sb.toString(); + } + } + + private void append(StringBuilder sb, String value) { + sb.append(value != null ? value : "").append("|"); + } + + public static class Builder { + private UUID uuid; + private String playerName; + private String ipAddress; + private String clientBrand; + private String locale; + private int viewDistance = 10; + private String mainHand = "RIGHT"; + private String osName; + private String osArch; + private String javaVersion; + private int cpuCores; + private int screenWidth; + private int screenHeight; + private String resourcePackHash; + private long firstSeen; + private long lastSeen; + + public Builder uuid(UUID uuid) { this.uuid = uuid; return this; } + public Builder playerName(String name) { this.playerName = name; return this; } + public Builder ipAddress(String ip) { this.ipAddress = ip; return this; } + public Builder clientBrand(String brand) { this.clientBrand = brand; return this; } + public Builder locale(String locale) { this.locale = locale; return this; } + public Builder viewDistance(int distance) { this.viewDistance = distance; return this; } + public Builder mainHand(String hand) { this.mainHand = hand; return this; } + public Builder osName(String os) { this.osName = os; return this; } + public Builder osArch(String arch) { this.osArch = arch; return this; } + public Builder javaVersion(String version) { this.javaVersion = version; return this; } + public Builder cpuCores(int cores) { this.cpuCores = cores; return this; } + public Builder screenWidth(int width) { this.screenWidth = width; return this; } + public Builder screenHeight(int height) { this.screenHeight = height; return this; } + public Builder resourcePackHash(String hash) { this.resourcePackHash = hash; return this; } + public Builder firstSeen(long time) { this.firstSeen = time; return this; } + public Builder lastSeen(long time) { this.lastSeen = time; return this; } + + public FingerprintData build() { + return new FingerprintData(this); + } + } +} diff --git a/src/main/java/com/security/fingerprint/FingerprintManager.java b/src/main/java/com/security/fingerprint/FingerprintManager.java new file mode 100644 index 0000000..2e9c50d --- /dev/null +++ b/src/main/java/com/security/fingerprint/FingerprintManager.java @@ -0,0 +1,114 @@ +package com.security.fingerprint; + +import org.bukkit.configuration.ConfigurationSection; + +import java.util.List; +import java.util.Objects; + +public class FingerprintManager { + + private final ConfigurationSection config; + + public FingerprintManager(ConfigurationSection config) { + this.config = config; + } + + public enum RiskLevel { + LOW(0, 30), + MEDIUM(30, 60), + HIGH(60, 100); + + private final int min; + private final int max; + + RiskLevel(int min, int max) { + this.min = min; + this.max = max; + } + + public boolean inRange(int score) { + return score >= min && score < max; + } + + public static RiskLevel fromScore(int score) { + for (RiskLevel level : values()) { + if (level.inRange(score)) return level; + } + return HIGH; + } + } + + public int calculateRiskScore(FingerprintData current, FingerprintData previous) { + if (previous == null) return 0; + + int score = 0; + + score += compareField("客户端品牌", current.getClientBrand(), previous.getClientBrand(), 20); + score += compareField("区域语言", current.getLocale(), previous.getLocale(), 5); + score += compareField("操作系统", current.getOsName(), previous.getOsName(), 15); + score += compareField("系统架构", current.getOsArch(), previous.getOsArch(), 10); + score += compareField("Java版本", current.getJavaVersion(), previous.getJavaVersion(), 5); + + if (current.getViewDistance() > 0 && previous.getViewDistance() > 0) { + int diff = Math.abs(current.getViewDistance() - previous.getViewDistance()); + if (diff > 4) score += 5; + } + + if (current.getCpuCores() > 0 && previous.getCpuCores() > 0 + && current.getCpuCores() != previous.getCpuCores()) { + score += 10; + } + + if (current.getScreenWidth() > 0 && previous.getScreenWidth() > 0) { + if (current.getScreenWidth() != previous.getScreenWidth() + || current.getScreenHeight() != previous.getScreenHeight()) { + score += 10; + } + } + + String hashCurrent = current.computeHash(); + String hashPrevious = previous.computeHash(); + + if (!Objects.equals(hashCurrent, hashPrevious)) { + score += 20; + } + + if (current.getIpAddress() != null && previous.getIpAddress() != null) { + String currentIpPrefix = current.getIpAddress().contains(".") + ? current.getIpAddress().substring(0, current.getIpAddress().lastIndexOf('.')) + : current.getIpAddress(); + String previousIpPrefix = previous.getIpAddress().contains(".") + ? previous.getIpAddress().substring(0, previous.getIpAddress().lastIndexOf('.')) + : previous.getIpAddress(); + + if (!currentIpPrefix.equals(previousIpPrefix)) { + score += 10; + } + } + + return Math.min(score, 100); + } + + private int compareField(String name, String current, String previous, int weight) { + if (current == null || previous == null) return 0; + if (current.equals(previous)) return 0; + if (current.equals("unknown") || previous.equals("unknown")) return weight / 2; + return weight; + } + + public boolean requiresAction(int score) { + return score >= config.getInt("risk-threshold", 60); + } + + public RiskLevel getRiskLevel(int score) { + return RiskLevel.fromScore(score); + } + + public String getActionForLevel(RiskLevel level) { + return switch (level) { + case LOW -> "allow"; + case MEDIUM -> config.getString("security.medium-risk-action", "warn"); + case HIGH -> config.getString("security.high-risk-action", "verify"); + }; + } +} diff --git a/src/main/java/com/security/listener/PlayerListener.java b/src/main/java/com/security/listener/PlayerListener.java new file mode 100644 index 0000000..a9bd156 --- /dev/null +++ b/src/main/java/com/security/listener/PlayerListener.java @@ -0,0 +1,130 @@ +package com.security.listener; + +import com.security.fingerprint.FingerprintCollector; +import com.security.fingerprint.FingerprintData; +import com.security.fingerprint.FingerprintManager; +import com.security.fingerprint.FingerprintManager.RiskLevel; +import com.security.storage.FingerprintLogger; +import com.security.storage.StorageManager; +import com.security.verification.VerificationManager; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.*; +import org.bukkit.plugin.Plugin; + +import java.util.UUID; + +public class PlayerListener implements Listener { + + private final Plugin plugin; + private final FingerprintCollector collector; + private final FingerprintManager fingerprintManager; + private final StorageManager storageManager; + private final VerificationManager verificationManager; + private final FingerprintLogger fingerprintLogger; + private final ConfigurationSection messages; + + public PlayerListener(Plugin plugin, FingerprintCollector collector, + FingerprintManager fingerprintManager, StorageManager storageManager, + VerificationManager verificationManager, FingerprintLogger fingerprintLogger, + ConfigurationSection messages) { + this.plugin = plugin; + this.collector = collector; + this.fingerprintManager = fingerprintManager; + this.storageManager = storageManager; + this.verificationManager = verificationManager; + this.fingerprintLogger = fingerprintLogger; + this.messages = messages; + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerLogin(PlayerLoginEvent event) { + Player player = event.getPlayer(); + UUID uuid = player.getUniqueId(); + + if (player.hasPermission("hardwarefingerprint.bypass")) { + fingerprintLogger.logRaw("玩家 " + player.getName() + " 已跳过指纹检查 (bypass权限)"); + return; + } + + FingerprintData currentFingerprint = collector.collect(player); + FingerprintData previousFingerprint = storageManager.loadFingerprint(uuid); + + if (previousFingerprint == null) { + storageManager.saveFingerprint(currentFingerprint); + storageManager.logLoginAttempt(uuid, currentFingerprint.getIpAddress(), + currentFingerprint.getClientBrand(), currentFingerprint.getLocale(), + 0, "first_login"); + fingerprintLogger.logFingerprint(currentFingerprint, 0, "first_login", "首次登录"); + return; + } + + int riskScore = fingerprintManager.calculateRiskScore(currentFingerprint, previousFingerprint); + RiskLevel level = fingerprintManager.getRiskLevel(riskScore); + String action = fingerprintManager.getActionForLevel(level); + + fingerprintLogger.logFingerprint(currentFingerprint, riskScore, action, level.name()); + + storageManager.logLoginAttempt(uuid, currentFingerprint.getIpAddress(), + currentFingerprint.getClientBrand(), currentFingerprint.getLocale(), + riskScore, action); + + switch (action) { + case "deny" -> { + event.disallow(PlayerLoginEvent.Result.KICK_OTHER, + colorize(messages.getString("kick-high-risk", "&c检测到异常登录行为!"))); + alertAdmin(player, riskScore); + } + case "verify" -> { + verificationManager.startVerification(player, riskScore, currentFingerprint); + event.disallow(PlayerLoginEvent.Result.KICK_OTHER, + colorize(messages.getString("kick-new-device", + "&c您正在使用新设备登录。\n&e请通过 &6/hfp verify <验证码> &e完成验证。"))); + alertAdmin(player, riskScore); + storageManager.saveFingerprint(currentFingerprint); + } + case "warn" -> { + alertAdmin(player, riskScore); + storageManager.saveFingerprint(currentFingerprint); + } + default -> { + storageManager.saveFingerprint(currentFingerprint); + } + } + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + if (verificationManager.isAwaitingVerification(player.getUniqueId())) { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + player.kickPlayer(colorize(messages.getString("verify-required", + "&e请先完成设备验证!"))); + }, 5L); + } + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + verificationManager.removeVerification(event.getPlayer().getUniqueId()); + } + + private void alertAdmin(Player player, int riskScore) { + String msg = colorize(messages.getString("admin-alert", + "&c⚠ 检测到高风险的登录行为!&7玩家: &e%player%&7, 风险评分: &c%score%") + .replace("%player%", player.getName()) + .replace("%score%", String.valueOf(riskScore))); + Bukkit.getOnlinePlayers().stream() + .filter(p -> p.hasPermission("hardwarefingerprint.admin")) + .forEach(p -> p.sendMessage(msg)); + plugin.getLogger().warning("高危登录: " + player.getName() + " (" + riskScore + "分)"); + } + + private String colorize(String text) { + return text != null ? text.replace("&", "§") : ""; + } +} diff --git a/src/main/java/com/security/storage/FingerprintLogger.java b/src/main/java/com/security/storage/FingerprintLogger.java new file mode 100644 index 0000000..c971d3b --- /dev/null +++ b/src/main/java/com/security/storage/FingerprintLogger.java @@ -0,0 +1,90 @@ +package com.security.storage; + +import com.security.fingerprint.FingerprintData; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class FingerprintLogger { + + private final Plugin plugin; + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + private PrintWriter writer; + private String currentDate; + + public FingerprintLogger(Plugin plugin) { + this.plugin = plugin; + rotateLog(); + } + + private void rotateLog() { + String today = fileDateFormat.format(new Date()); + if (currentDate != null && currentDate.equals(today) && writer != null) return; + + if (writer != null) { + writer.close(); + } + + File logDir = new File(plugin.getDataFolder(), "logs"); + if (!logDir.exists()) logDir.mkdirs(); + + try { + File logFile = new File(logDir, "fingerprint-" + today + ".log"); + writer = new PrintWriter(new FileWriter(logFile, true), true); + currentDate = today; + } catch (Exception e) { + plugin.getLogger().severe("无法创建指纹日志文件: " + e.getMessage()); + } + } + + public void logFingerprint(FingerprintData data, int riskScore, String action, String riskLabel) { + rotateLog(); + if (writer == null) return; + + String now = dateFormat.format(new Date()); + String separator = " | "; + + StringBuilder sb = new StringBuilder(); + sb.append("[").append(now).append("]"); + sb.append(separator).append("玩家: ").append(nullSafe(data.getPlayerName())); + sb.append(separator).append("UUID: ").append(nullSafe(data.getUuid() != null ? data.getUuid().toString() : null)); + sb.append(separator).append("IP: ").append(nullSafe(data.getIpAddress())); + sb.append(separator).append("客户端: ").append(nullSafe(data.getClientBrand())); + sb.append(separator).append("区域语言: ").append(nullSafe(data.getLocale())); + sb.append(separator).append("视野距离: ").append(data.getViewDistance()); + sb.append(separator).append("惯用手: ").append(nullSafe(data.getMainHand())); + sb.append(separator).append("操作系统: ").append(nullSafe(data.getOsName())); + sb.append(separator).append("系统架构: ").append(nullSafe(data.getOsArch())); + sb.append(separator).append("Java版本: ").append(nullSafe(data.getJavaVersion())); + sb.append(separator).append("CPU核心: ").append(data.getCpuCores() > 0 ? String.valueOf(data.getCpuCores()) : "未知"); + sb.append(separator).append("分辨率: ").append(data.getScreenWidth() > 0 ? data.getScreenWidth() + "x" + data.getScreenHeight() : "未知"); + sb.append(separator).append("指纹哈希: ").append(nullSafe(data.computeHash())); + sb.append(separator).append("风险评分: ").append(riskScore); + sb.append(separator).append("风险等级: ").append(riskLabel); + sb.append(separator).append("处理动作: ").append(action); + + writer.println(sb); + } + + public void logRaw(String message) { + rotateLog(); + if (writer == null) return; + String now = dateFormat.format(new Date()); + writer.println("[" + now + "] " + message); + } + + private String nullSafe(String value) { + return value != null ? value : "未知"; + } + + public void close() { + if (writer != null) { + writer.close(); + } + } +} diff --git a/src/main/java/com/security/storage/StorageManager.java b/src/main/java/com/security/storage/StorageManager.java new file mode 100644 index 0000000..744ac13 --- /dev/null +++ b/src/main/java/com/security/storage/StorageManager.java @@ -0,0 +1,224 @@ +package com.security.storage; + +import com.security.fingerprint.FingerprintData; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class StorageManager { + + private final Plugin plugin; + private Connection connection; + private final Map cache = new ConcurrentHashMap<>(); + + public StorageManager(Plugin plugin, ConfigurationSection config) { + this.plugin = plugin; + initDatabase(config); + } + + private void initDatabase(ConfigurationSection config) { + String type = config.getString("type", "sqlite"); + try { + if (type.equalsIgnoreCase("mysql")) { + connectMySQL(config.getConfigurationSection("mysql")); + } else { + connectSQLite(); + } + createTables(); + } catch (SQLException e) { + plugin.getLogger().severe("数据库初始化失败: " + e.getMessage()); + } + } + + private void connectSQLite() throws SQLException { + File dataFolder = plugin.getDataFolder(); + if (!dataFolder.exists()) dataFolder.mkdirs(); + String url = "jdbc:sqlite:" + new File(dataFolder, "fingerprints.db").getAbsolutePath(); + connection = DriverManager.getConnection(url); + plugin.getLogger().info("SQLite 数据库已连接"); + } + + private void connectMySQL(ConfigurationSection mysql) throws SQLException { + String host = mysql.getString("host", "localhost"); + int port = mysql.getInt("port", 3306); + String database = mysql.getString("database", "minecraft_security"); + String username = mysql.getString("username", "root"); + String password = mysql.getString("password", ""); + String url = "jdbc:mysql://" + host + ":" + port + "/" + database + + "?useSSL=false&characterEncoding=utf8"; + connection = DriverManager.getConnection(url, username, password); + plugin.getLogger().info("MySQL 数据库已连接"); + } + + private void createTables() throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.execute( + "CREATE TABLE IF NOT EXISTS fingerprints (" + + " uuid VARCHAR(36) PRIMARY KEY," + + " player_name VARCHAR(32) NOT NULL," + + " ip_address VARCHAR(45)," + + " client_brand VARCHAR(64)," + + " locale VARCHAR(16)," + + " view_distance INT DEFAULT 10," + + " main_hand VARCHAR(8) DEFAULT 'RIGHT'," + + " os_name VARCHAR(64)," + + " os_arch VARCHAR(16)," + + " java_version VARCHAR(32)," + + " cpu_cores INT DEFAULT 0," + + " screen_width INT DEFAULT 0," + + " screen_height INT DEFAULT 0," + + " resource_pack_hash VARCHAR(128)," + + " first_seen BIGINT NOT NULL," + + " last_seen BIGINT NOT NULL," + + " fingerprint_hash VARCHAR(256)" + + ")" + ); + stmt.execute( + "CREATE TABLE IF NOT EXISTS login_history (" + + " id INTEGER PRIMARY KEY AUTOINCREMENT," + + " uuid VARCHAR(36) NOT NULL," + + " ip_address VARCHAR(45)," + + " client_brand VARCHAR(64)," + + " locale VARCHAR(16)," + + " risk_score INT DEFAULT 0," + + " action_taken VARCHAR(16)," + + " login_time BIGINT NOT NULL," + + " fingerprint_hash VARCHAR(256)" + + ")" + ); + } + } + + public void saveFingerprint(FingerprintData data) { + cache.put(data.getUuid(), data); + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO fingerprints (uuid, player_name, ip_address, client_brand, locale, " + + "view_distance, main_hand, os_name, os_arch, java_version, cpu_cores, " + + "screen_width, screen_height, resource_pack_hash, first_seen, last_seen, fingerprint_hash) " + + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) " + + "ON CONFLICT(uuid) DO UPDATE SET " + + "player_name=excluded.player_name, ip_address=excluded.ip_address, " + + "client_brand=excluded.client_brand, locale=excluded.locale, " + + "view_distance=excluded.view_distance, main_hand=excluded.main_hand, " + + "os_name=excluded.os_name, os_arch=excluded.os_arch, " + + "java_version=excluded.java_version, cpu_cores=excluded.cpu_cores, " + + "screen_width=excluded.screen_width, screen_height=excluded.screen_height, " + + "resource_pack_hash=excluded.resource_pack_hash, last_seen=excluded.last_seen, " + + "fingerprint_hash=excluded.fingerprint_hash" + )) { + ps.setString(1, data.getUuid().toString()); + ps.setString(2, data.getPlayerName()); + ps.setString(3, data.getIpAddress()); + ps.setString(4, data.getClientBrand()); + ps.setString(5, data.getLocale()); + ps.setInt(6, data.getViewDistance()); + ps.setString(7, data.getMainHand()); + ps.setString(8, data.getOsName()); + ps.setString(9, data.getOsArch()); + ps.setString(10, data.getJavaVersion()); + ps.setInt(11, data.getCpuCores()); + ps.setInt(12, data.getScreenWidth()); + ps.setInt(13, data.getScreenHeight()); + ps.setString(14, data.getResourcePackHash()); + ps.setLong(15, data.getFirstSeen() > 0 ? data.getFirstSeen() : data.getLastSeen()); + ps.setLong(16, data.getLastSeen()); + ps.setString(17, data.computeHash()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().warning("保存指纹失败: " + e.getMessage()); + } + } + + public FingerprintData loadFingerprint(UUID uuid) { + if (cache.containsKey(uuid)) return cache.get(uuid); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT * FROM fingerprints WHERE uuid = ?" + )) { + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + FingerprintData data = mapRowToFingerprint(rs); + cache.put(uuid, data); + return data; + } + } catch (SQLException e) { + plugin.getLogger().warning("加载指纹失败: " + e.getMessage()); + } + return null; + } + + public void logLoginAttempt(UUID uuid, String ip, String brand, String locale, + int riskScore, String action) { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO login_history (uuid, ip_address, client_brand, locale, " + + "risk_score, action_taken, login_time) VALUES (?,?,?,?,?,?,?)" + )) { + ps.setString(1, uuid.toString()); + ps.setString(2, ip); + ps.setString(3, brand); + ps.setString(4, locale); + ps.setInt(5, riskScore); + ps.setString(6, action); + ps.setLong(7, System.currentTimeMillis()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().warning("记录登录历史失败: " + e.getMessage()); + } + } + + public List> getLoginHistory(UUID uuid, int limit) { + List> history = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT * FROM login_history WHERE uuid = ? ORDER BY login_time DESC LIMIT ?" + )) { + ps.setString(1, uuid.toString()); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map entry = new HashMap<>(); + entry.put("ip", rs.getString("ip_address")); + entry.put("brand", rs.getString("client_brand")); + entry.put("locale", rs.getString("locale")); + entry.put("risk", rs.getInt("risk_score")); + entry.put("action", rs.getString("action_taken")); + entry.put("time", rs.getLong("login_time")); + history.add(entry); + } + } catch (SQLException e) { + plugin.getLogger().warning("查询登录历史失败: " + e.getMessage()); + } + return history; + } + + private FingerprintData mapRowToFingerprint(ResultSet rs) throws SQLException { + return new FingerprintData.Builder() + .uuid(UUID.fromString(rs.getString("uuid"))) + .playerName(rs.getString("player_name")) + .ipAddress(rs.getString("ip_address")) + .clientBrand(rs.getString("client_brand")) + .locale(rs.getString("locale")) + .viewDistance(rs.getInt("view_distance")) + .mainHand(rs.getString("main_hand")) + .osName(rs.getString("os_name")) + .osArch(rs.getString("os_arch")) + .javaVersion(rs.getString("java_version")) + .cpuCores(rs.getInt("cpu_cores")) + .screenWidth(rs.getInt("screen_width")) + .screenHeight(rs.getInt("screen_height")) + .resourcePackHash(rs.getString("resource_pack_hash")) + .firstSeen(rs.getLong("first_seen")) + .lastSeen(rs.getLong("last_seen")) + .build(); + } + + public void close() { + cache.clear(); + if (connection != null) { + try { connection.close(); } catch (SQLException ignored) {} + } + } +} diff --git a/src/main/java/com/security/verification/VerificationManager.java b/src/main/java/com/security/verification/VerificationManager.java new file mode 100644 index 0000000..5b3f58a --- /dev/null +++ b/src/main/java/com/security/verification/VerificationManager.java @@ -0,0 +1,142 @@ +package com.security.verification; + +import com.security.fingerprint.FingerprintData; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.security.SecureRandom; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class VerificationManager { + + private static class VerificationSession { + final String code; + final int riskScore; + final FingerprintData fingerprint; + int attempts; + long expiresAt; + + VerificationSession(String code, int riskScore, FingerprintData fingerprint, long timeoutMs) { + this.code = code; + this.riskScore = riskScore; + this.fingerprint = fingerprint; + this.attempts = 0; + this.expiresAt = System.currentTimeMillis() + timeoutMs; + } + + boolean isExpired() { + return System.currentTimeMillis() > expiresAt; + } + } + + private final Plugin plugin; + private final ConfigurationSection config; + private final Map pendingVerifications = new ConcurrentHashMap<>(); + private final SecureRandom random = new SecureRandom(); + + public VerificationManager(Plugin plugin, ConfigurationSection config) { + this.plugin = plugin; + this.config = config; + } + + public void startVerification(Player player, int riskScore, FingerprintData fingerprint) { + String code = generateCode(); + long timeoutMs = config.getInt("code-timeout", 120) * 1000L; + pendingVerifications.put(player.getUniqueId(), + new VerificationSession(code, riskScore, fingerprint, timeoutMs)); + + plugin.getLogger().info("玩家 " + player.getName() + " 需要二次验证,验证码: " + code); + + if (player.isOnline()) { + player.sendMessage("§8[§b安全系统§8] §e您的验证码是: §6" + code); + player.sendMessage("§7请使用 §6/hfp verify " + code + " §7重新登录完成验证"); + player.sendMessage("§7验证码 " + (timeoutMs / 1000) + " 秒内有效"); + } + } + + public boolean verify(Player player, String code) { + UUID uuid = player.getUniqueId(); + VerificationSession session = pendingVerifications.get(uuid); + + if (session == null) { + player.sendMessage("§c您没有待处理的验证请求,请重新登录。"); + return false; + } + + if (session.isExpired()) { + pendingVerifications.remove(uuid); + player.sendMessage("§c验证码已过期,请重新登录。"); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (player.isOnline()) player.kickPlayer("§c验证超时"); + }, 5L); + return false; + } + + session.attempts++; + if (!session.code.equals(code)) { + int maxAttempts = config.getInt("max-attempts", 3); + int remaining = maxAttempts - session.attempts; + if (remaining <= 0) { + pendingVerifications.remove(uuid); + player.sendMessage("§c验证失败次数过多,已断开连接。"); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (player.isOnline()) player.kickPlayer("§c验证失败次数过多"); + }, 5L); + } else { + player.sendMessage("§c验证码错误!剩余尝试次数: " + remaining); + } + return false; + } + + pendingVerifications.remove(uuid); + sendSuccessMessage(player); + + int passDuration = config.getInt("pass-duration", 30); + if (passDuration > 0) { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (player.isOnline()) { + player.sendMessage("§8[§b安全系统§8] §e设备验证已过期,下次登录将重新验证。"); + } + }, passDuration * 60L * 20L); + } + + return true; + } + + public boolean isAwaitingVerification(UUID uuid) { + VerificationSession session = pendingVerifications.get(uuid); + if (session == null) return false; + if (session.isExpired()) { + pendingVerifications.remove(uuid); + return false; + } + return true; + } + + public void removeVerification(UUID uuid) { + pendingVerifications.remove(uuid); + } + + private String generateCode() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 6; i++) { + sb.append(random.nextInt(10)); + } + return sb.toString(); + } + + private void sendSuccessMessage(Player player) { + String msg = config.getString("prefix", "&8[&b安全系统&8]&r ") + "&a设备验证成功!欢迎回来。"; + player.sendMessage(msg.replace("&", "§")); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (player.isOnline()) { + player.sendMessage("§8[§b安全系统§8] §7您的设备已通过验证,可正常游戏。"); + } + }, 10L); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..26e47e0 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,74 @@ +# =========================================== +# HardwareFingerprint 插件配置文件 v1.0 +# =========================================== + +# 指纹收集设置 +fingerprint: + # 是否收集客户端品牌 (Vanilla/Forge/Lunar/Fabric 等) + collect-client-brand: true + # 是否检查玩家的资源包哈希 (需 ProtocolLib) + collect-resource-pack: true + # 是否记录屏幕分辨率 + collect-screen-resolution: true + +# 安全策略 +security: + # 风险评分阈值 (0-100),超过此分数将触发操作 + risk-threshold: 60 + # 与历史指纹差异过大时的操作 + # deny - 直接踢出 + # verify - 要求二次验证 + # warn - 仅警告管理员 + high-risk-action: "verify" + # 中等风险操作 (30-60分) + medium-risk-action: "warn" + # 是否踢出风险分数超过阈值的玩家 + kick-on-high-risk: false + +# 二次验证设置 +verification: + # 验证方式: "code" (验证码) 或 "command" (命令问答) + method: "code" + # 验证码超时时间 (秒) + code-timeout: 120 + # 最大尝试次数 + max-attempts: 3 + # 验证成功后是否临时放行 (分钟) + pass-duration: 30 + # 自定义验证问题 (method: command 时有效) + questions: + - "请输入您注册时使用的邮箱后缀" + - "您上次登录的IP地址最后一段是多少" + +# 数据库 +database: + # 存储类型: sqlite (内置) / mysql (需自行配置) + type: sqlite + # MySQL 配置 (仅 type: mysql 时生效) + mysql: + host: localhost + port: 3306 + database: minecraft_security + username: root + password: "" + +# 日志记录 +logging: + # 是否记录指纹日志文件 (保存到 plugins/HardwareFingerprint/logs/) + enabled: true + # 日志格式: detailed (详细) / compact (精简) + format: detailed + # 是否同时记录到服务端控制台日志 + log-to-console: true + # 日志保留天数 (0=永久保留) + keep-days: 30 + +# 消息配置 (支持 & 颜色代码) +messages: + prefix: "&8[&b安全系统&8]&r " + kick-new-device: "&c您正在使用新设备登录。\n&e请通过 &6/hfp verify <验证码> &e完成验证。\n&7验证码已发送至您绑定的联系方式。" + kick-high-risk: "&c检测到异常登录行为!\n&e您的登录已被拒绝。\n&7如确认是本人,请联系管理员。" + verify-success: "&a设备验证成功!欢迎回来, %player%" + verify-fail: "&c验证失败!请重试。剩余次数: %attempts%" + verify-required: "&e请先完成设备验证!使用 &6/hfp verify <验证码>&e 完成验证。" + admin-alert: "&c⚠ 检测到高风险的登录行为!&7玩家: &e%player%&7, 风险评分: &c%score%" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..679d6e6 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,25 @@ +name: HardwareFingerprint +version: 1.0.0 +main: com.security.HardwareFingerprintPlugin +api-version: 1.21 +softdepend: + - ProtocolLib +author: SecurityTeam +description: 硬件指纹收集与二次安全验证插件 - 抵御恶意攻击与盗号登录 + +commands: + hfp: + description: 指纹系统主命令 + aliases: [hardwarefingerprint, fingerprint] + usage: /hfp + +permissions: + hardwarefingerprint.admin: + description: 管理员权限 + default: op + hardwarefingerprint.verify.bypass: + description: 跳过二次验证 + default: false + hardwarefingerprint.bypass: + description: 跳过所有指纹检查 + default: false