Initial commit: HardwareFingerprint:Paper 服务端插件(Java 21),基于硬件指纹的服务端校验
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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%"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user