Initial commit: HardwareFingerprint:Paper 服务端插件(Java 21),基于硬件指纹的服务端校验

This commit is contained in:
WpyQwq
2026-09-19 12:06:47 +08:00
commit 86ce6cc302
11 changed files with 1305 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.security</groupId>
<artifactId>HardwareFingerprint</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>HardwareFingerprint-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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<Map<String, Object>> 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<String, Object> 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());
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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");
};
}
}
@@ -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("&", "§") : "";
}
}
@@ -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();
}
}
}
@@ -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<UUID, FingerprintData> 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<Map<String, Object>> getLoginHistory(UUID uuid, int limit) {
List<Map<String, Object>> 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<String, Object> 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) {}
}
}
}
@@ -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<UUID, VerificationSession> 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);
}
}
+74
View File
@@ -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%"
+25
View File
@@ -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 <verify|info|history|reload|status>
permissions:
hardwarefingerprint.admin:
description: 管理员权限
default: op
hardwarefingerprint.verify.bypass:
description: 跳过二次验证
default: false
hardwarefingerprint.bypass:
description: 跳过所有指纹检查
default: false