V4.0 安全扩展:29个新模块 + 60项API端点 + 164项新测试全部通过

新增模块(5组29个):

反作弊扩展(6个):
- AntiKillAuraService: 杀戮光环检测(攻击频率/目标切换/旋转角度)
- AntiReachService: 超距攻击检测(攻击距离/位置回溯验证)
- AntiSpeedService: 水平速度检测(移动速度/GroundSpoof异常)
- AntiJesusService: 水上行走检测(水面/岩浆面移动验证)
- AntiNoFallService: 无摔落伤害检测(落地检测/伤害验证)
- AntiScaffoldService: 自动搭路检测(方块放置频率/角度/速度)

玩家行为安全(6个):
- AntiNukerService: 快速破坏检测(挖矿速度/范围/模式识别)
- AntiAutoClickerService: 自动点击检测(CPS统计/点击间隔熵值分析)
- AntiChestStealService: 自动偷箱检测(开箱/取物速度模式识别)
- AntiAutoFishService: 自动钓鱼检测(钓鱼行为时序/反应速度)
- AntiInventoryManipulationService: 背包操作检测(非法丢弃/槽位异常)
- AntiBaritoneService: AI寻路机器人检测(路径平滑度/行为重复率)

服务器保护(6个):
- PacketFloodProtectionService: 数据包洪水防护(窗口/递增惩罚机制)
- AntiSignExploitService: 告示牌漏洞防护(超长JSON/NBT/命令注入)
- AntiBookBanService: 书与笔漏洞防护(超大页码/翻译键滥用/命令注入)
- AntiResourcePackExploitService: 资源包漏洞防护(内网SSRF/SHA-1黑名单)
- AntiTabCompleteCrashService: Tab补全崩溃防护(深度嵌套/洪水/无效UTF-8)
- AntiOfflineModeSpoofService: 离线UUID欺诈防护(格式校验/IP-UUID关联)

访问控制(6个):
- OPPrivilegeMonitorService: OP权限监控(权限变更/敏感命令审计)
- ConfigTamperDetectionService: 配置文件篡改检测(SHA-256基线监控)
- BackdoorPluginScannerService: 后门插件扫描(恶意类名/远程执行检测)
- AntiVPNProxyService: VPN代理检测(托管ASN/IP范围匹配)
- AntiAltAccountService: 小号检测(IP关联/行为相似度/封禁逃逸)
- AntiNameSpoofService: 名称冒充检测(Unicode同形字/空白字符/编辑距离)

聊天社交安全(5个):
- ChatFloodProtectionService: 聊天洪水防护(双窗口频率/相似度/递增静音)
- AntiAdvertisementService: 广告检测(IP/域名/QQ群/Discord邀请正则)
- AntiPhishingLinkService: 钓鱼链接检测(短链接/可疑域名/显示文本不匹配)
- AntiCommandAbuseService: 命令滥用检测(洪水/插件探测/Shell注入)
- PlayerPrivacyService: 玩家隐私保护(IP脱敏/GDPR合规/数据保留策略)

配置与API:
- ServerGuardConfig新增29个boolean开关(默认true),全部独立可控
- application.yml新增29行YAML配置(含中文注释说明功能)
- DashboardController整合29个新服务,新增60个REST API端点(总计108个端点)
- 所有模块使用双构造函数(无参+@Autowired)、静态结果类工厂方法

测试:新增5个测试类164项测试,全部通过(全项目278项/17测试类/159源文件)

修复:
- AntiBookBanService: helper方法改为返回int得分贡献,修复translate/score/clickEvent检测不生效
- AntiResourcePackExploitService: 修复Set.of()中.vbs重复导致ExceptionInInitializer
- AntiTabCompleteCrashService: 深度嵌套得分提高至75(触发blocked阈值)
- AntiOfflineModeSpoofService: 快速UUID切换得分提高至75(触发blocked阈值)
- PacketFloodProtectionService: 重构检测流程,移除提前返回导致的检测屏蔽
This commit is contained in:
Peijun Zhao
2026-05-16 11:47:59 +08:00
parent f82fb616cc
commit 1269d4dfa5
37 changed files with 13890 additions and 1 deletions
@@ -0,0 +1,413 @@
package com.aluer.security;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* V4.0 访问控制模块测试类
* 覆盖 OPPrivilegeMonitor, ConfigTamperDetection, BackdoorPluginScanner,
* AntiVPNProxy, AntiAltAccount, AntiNameSpoof 六个安全服务
*/
class V40AccessControlTest {
// ==================== OPPrivilegeMonitorService Tests ====================
@Test
void opMonitorShouldDetectUnauthorizedGrantViaRcon() {
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
// OP granted via RCON (non-console source) should be flagged suspicious
OPPrivilegeMonitorService.OPMonitorResult result = service.recordOPChange(
"NewAdmin", 4, "grant", "RCON", "UnknownPlayer");
assertTrue(result.isSuspicious());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("UNAUTHORIZED_OP_GRANT")));
}
@Test
void opMonitorShouldAllowConsoleGrant() {
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
// OP granted via CONSOLE should be considered clean
OPPrivilegeMonitorService.OPMonitorResult result = service.recordOPChange(
"LegitAdmin", 4, "grant", "CONSOLE", "Server");
assertTrue(result.isClean());
}
@Test
void opMonitorShouldDetectOpLevelJump() {
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
// First grant Level 1 via console (clean)
service.recordOPChange("Player1", 1, "grant", "CONSOLE", "Server");
// Then jump to Level 4 via console — should be suspicious
OPPrivilegeMonitorService.OPMonitorResult result = service.recordOPChange(
"Player1", 4, "grant", "CONSOLE", "Server");
assertTrue(result.isSuspicious());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("OP_LEVEL_JUMP")));
}
@Test
void opMonitorShouldDetectCommandStorm() {
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
// Simulate 6 sensitive commands from same player within 1 minute
for (int i = 0; i < 6; i++) {
OPPrivilegeMonitorService.OPMonitorResult result = service.recordSensitiveCommand(
"SuspiciousAdmin", "ban Player" + i, "IN_GAME");
if (i >= 5) {
assertTrue(result.isSuspicious());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("COMMAND_STORM")));
}
}
}
@Test
void opMonitorShouldIdentifySensitiveCommands() {
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
assertTrue(service.isSensitiveCommand("ban BadPlayer"));
assertTrue(service.isSensitiveCommand("/stop"));
assertTrue(service.isSensitiveCommand("/reload"));
assertTrue(service.isSensitiveCommand("/whitelist add Someone"));
assertFalse(service.isSensitiveCommand("list"));
assertFalse(service.isSensitiveCommand("say hello"));
}
@Test
void opMonitorGetStatusReturnsCorrectStructure() {
OPPrivilegeMonitorService service = new OPPrivilegeMonitorService();
service.recordOPChange("Admin1", 4, "grant", "CONSOLE", "Server");
service.recordOPChange("Admin2", 2, "grant", "CONSOLE", "Server");
var status = service.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalOps"));
assertTrue(status.containsKey("opChanges"));
assertTrue(status.containsKey("sensitiveCommandLog"));
assertEquals(2L, status.get("totalOps"));
}
// ==================== ConfigTamperDetectionService Tests ====================
@Test
void configTamperShouldDetectHashMismatch() {
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
byte[] original = "online-mode=true\nmax-players=20\n".getBytes();
byte[] tampered = "online-mode=false\nmax-players=20\n".getBytes();
service.registerBaseline("server.properties", original);
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
"server.properties", tampered, "system");
assertTrue(result.isTampered());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("HASH_MISMATCH")));
}
@Test
void configTamperShouldDetectOnlineModeDisabled() {
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
byte[] props = "online-mode=false\nenable-rcon=true\nrcon.password=123\n".getBytes();
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
"server.properties", props, "system");
assertTrue(result.isTampered());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("ONLINE_MODE_DISABLED")));
}
@Test
void configTamperShouldDetectWeakRconPassword() {
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
byte[] props = "online-mode=true\nenable-rcon=true\nrcon.password=abc\n".getBytes();
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
"server.properties", props, "system");
assertTrue(result.isTampered());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("WEAK_RCON_PASSWORD")));
}
@Test
void configTamperShouldDetectSecureProfileDisabled() {
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
byte[] props = "online-mode=true\nenforce-secure-profile=false\n".getBytes();
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
"server.properties", props, "system");
assertTrue(result.isTampered());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("SECURE_PROFILE_DISABLED")));
}
@Test
void configTamperShouldAllowCleanConfig() {
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
byte[] props = "online-mode=true\nmax-players=20\nenforce-secure-profile=true\n".getBytes();
service.registerBaseline("server.properties", props);
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
"server.properties", props, "system");
assertTrue(result.isClean());
}
@Test
void configTamperShouldDetectUnauthorizedOpInOpsJson() {
ConfigTamperDetectionService service = new ConfigTamperDetectionService();
service.setLegitimateOps(Set.of("Owner", "Admin1"));
byte[] opsJson = ("[{\"uuid\":\"aaa-bbb-ccc\",\"name\":\"UnknownHacker\",\"level\":4}]").getBytes();
ConfigTamperDetectionService.TamperDetectionResult result = service.checkFileIntegrity(
"ops.json", opsJson, "system");
assertTrue(result.isTampered());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("UNAUTHORIZED_OP")));
}
// ==================== BackdoorPluginScannerService Tests ====================
@Test
void backdoorScannerShouldDetectKnownMaliciousPlugin() {
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
BackdoorPluginScannerService.PluginScanResult result = service.scanPlugin(
"ForceOP", "ForceOP.jar", true, "com.force.op.Main",
List.of("com.force.op.Main"), List.of());
assertTrue(result.isMalicious());
assertEquals(BackdoorPluginScannerService.RiskLevel.MALICIOUS, result.getRiskLevel());
}
@Test
void backdoorScannerShouldDetectMissingPluginYml() {
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
BackdoorPluginScannerService.PluginScanResult result = service.scanPlugin(
"MysteryPlugin", "MysteryPlugin.jar", false, "com.test.Main",
List.of("com.test.Main"), List.of());
assertTrue(result.isSuspicious());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("MISSING_PLUGIN_YML")));
}
@Test
void backdoorScannerShouldDetectRCECapability() {
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
BackdoorPluginScannerService.PluginScanResult result = service.scanPlugin(
"RemoteTool", "RemoteTool.jar", true, "com.remote.Main",
List.of("com.remote.Main", "Runtime.exec.Payload", "com.remote.Cleanup"),
List.of());
assertTrue(result.isSuspicious());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("RCE_CAPABILITY")));
}
@Test
void backdoorScannerShouldDetectSuspiciousConfigKeys() {
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
BackdoorPluginScannerService.PluginScanResult result = service.scanPlugin(
"NormalPlugin", "NormalPlugin.jar", true, "com.normal.Main",
List.of("com.normal.Main"),
List.of("database-url", "backdoor-enabled", "webhook-url"));
assertTrue(result.isSuspicious());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("SUSPICIOUS_CONFIG_KEY")));
}
@Test
void backdoorScannerShouldAllowSafePlugin() {
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
BackdoorPluginScannerService.PluginScanResult result = service.scanPlugin(
"WorldEdit", "WorldEdit.jar", true, "com.sk89q.worldedit.bukkit.WorldEditPlugin",
List.of("com.sk89q.worldedit.bukkit.WorldEditPlugin"), List.of("max-blocks", "nav-wand"));
assertTrue(result.isSafe());
assertEquals(BackdoorPluginScannerService.RiskLevel.SAFE, result.getRiskLevel());
}
@Test
void backdoorScannerQuickScanShouldDetectMalicious() {
BackdoorPluginScannerService service = new BackdoorPluginScannerService();
BackdoorPluginScannerService.PluginScanResult result = service.quickScan("OPMe");
assertTrue(result.isMalicious());
}
// ==================== AntiVPNProxyService Tests ====================
@Test
void antiVpnShouldDetectDigitalOceanASN() {
AntiVPNProxyService service = new AntiVPNProxyService();
// AS14061 = DigitalOcean
AntiVPNProxyService.VPNCheckResult result = service.checkIP(
"159.65.100.50", "14061", "DigitalOcean, LLC", "Player1");
assertTrue(result.isHosting());
assertFalse(result.isClean());
}
@Test
void antiVpnShouldDetectDatacenterIPRange() {
AntiVPNProxyService service = new AntiVPNProxyService();
// Vultr IP prefix
AntiVPNProxyService.VPNCheckResult result = service.checkIP(
"45.76.100.200", null, null, "Player2");
assertTrue(result.isHosting());
}
@Test
void antiVpnShouldAllowTrustedIP() {
AntiVPNProxyService service = new AntiVPNProxyService();
// Add to trusted list first
service.addTrustedIP("159.65.100.50");
AntiVPNProxyService.VPNCheckResult result = service.checkIP(
"159.65.100.50", "14061", "DigitalOcean, LLC", "Player3");
assertTrue(result.isClean());
}
@Test
void antiVpnShouldDetectHostingISP() {
AntiVPNProxyService service = new AntiVPNProxyService();
AntiVPNProxyService.VPNCheckResult result = service.checkIP(
"198.51.100.50", null, "SomeHosting Cloud LTD", "Player4");
assertTrue(result.isHosting());
}
@Test
void antiVpnShouldAllowResidentialIP() {
AntiVPNProxyService service = new AntiVPNProxyService();
AntiVPNProxyService.VPNCheckResult result = service.checkIP(
"203.0.113.1", "12345", "Comcast Cable", "LegitPlayer");
assertTrue(result.isClean());
}
// ==================== AntiAltAccountService Tests ====================
@Test
void antiAltShouldDetectBanEvasion() {
AntiAltAccountService service = new AntiAltAccountService();
// Ban this IP first
service.recordBannedIP("10.0.0.100", "Griefing");
// New player joins from the same IP immediately after
AntiAltAccountService.AltAccountResult result = service.onPlayerLogin(
"NewAltPlayer", "uuid-new", "10.0.0.100");
assertTrue(result.isAltDetected());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("BAN_EVASION")));
}
@Test
void antiAltShouldDetectRapidAccountSwitch() {
AntiAltAccountService service = new AntiAltAccountService();
// Same IP, multiple accounts within 5 minutes
service.onPlayerLogin("Alt1", "uuid-1", "10.0.0.200");
service.onPlayerLogin("Alt2", "uuid-2", "10.0.0.200");
AntiAltAccountService.AltAccountResult result = service.onPlayerLogin(
"Alt3", "uuid-3", "10.0.0.200");
assertTrue(result.isAltDetected());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("RAPID_ACCOUNT_SWITCH")));
}
@Test
void antiAltShouldAllowSingleAccountLogin() {
AntiAltAccountService service = new AntiAltAccountService();
AntiAltAccountService.AltAccountResult result = service.onPlayerLogin(
"LegitPlayer", "uuid-real", "192.168.1.1");
assertTrue(result.isClean());
}
@Test
void antiAltShouldTrackTroublemakerIPs() {
AntiAltAccountService service = new AntiAltAccountService();
// First, login with account and create IP association
service.onPlayerLogin("Griefer1", "uuid-g1", "10.0.0.50");
// Ban the IP
service.recordBannedIP("10.0.0.50", "Griefing");
// Mark as troublemaker
service.markAsTroublemaker("Griefer1");
// New account from same IP should be detected
AntiAltAccountService.AltAccountResult result = service.onPlayerLogin(
"GrieferAlt", "uuid-g2", "10.0.0.50");
assertTrue(result.isAltDetected());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("TROUBLEMAKER_IP")));
}
@Test
void antiAltShouldReturnAssociatedAccounts() {
AntiAltAccountService service = new AntiAltAccountService();
service.onPlayerLogin("AccA", "uuid-a", "10.0.0.77");
service.onPlayerLogin("AccB", "uuid-b", "10.0.0.77");
Set<String> accounts = service.getAccountsForIP("10.0.0.77");
assertTrue(accounts.contains("AccA"));
assertTrue(accounts.contains("AccB"));
}
// ==================== AntiNameSpoofService Tests ====================
@Test
void antiNameSpoofShouldDetectUnicodeHomoglyph() {
AntiNameSpoofService service = new AntiNameSpoofService();
service.addProtectedName("Admin");
// Use Cyrillic 'a' (U+0430) instead of Latin 'a'
String spoofedName = "Admin"; // Note: uses Cyrillic 'a' when built with proper Unicode
// For the test, we check a player name that contains Unicode normalization differences
AntiNameSpoofService.NameSpoofResult result = service.checkPlayerName(
"Admın", "10.0.0.1"); // Uses dotless i (U+0131) instead of normal i
assertTrue(result.isSpoofed());
}
@Test
void antiNameSpoofShouldDetectInvisibleCharacters() {
AntiNameSpoofService service = new AntiNameSpoofService();
// Insert zero-width space U+200B into the name
String spoofedName = "Admin​test";
AntiNameSpoofService.NameSpoofResult result = service.checkPlayerName(
spoofedName, "10.0.0.2");
assertTrue(result.isSpoofed());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("INVISIBLE_CHARS")));
}
@Test
void antiNameSpoofShouldAllowNormalPlayerName() {
AntiNameSpoofService service = new AntiNameSpoofService();
service.addProtectedName("Admin");
AntiNameSpoofService.NameSpoofResult result = service.checkPlayerName(
"Player123", "10.0.0.3");
assertTrue(result.isClean());
}
@Test
void antiNameSpoofShouldDetectLevenshteinSimilarity() {
AntiNameSpoofService service = new AntiNameSpoofService();
service.addProtectedName("Administrator");
// "Administrat0r" (using zero instead of 'o') is very similar
AntiNameSpoofService.NameSpoofResult result = service.checkPlayerName(
"Administrat0r", "10.0.0.4");
assertTrue(result.isSpoofed());
assertTrue(result.getReasons().stream().anyMatch(r -> r.contains("SIMILARITY_SPOOF")));
}
@Test
void antiNameSpoofShouldBlockSpoofedName() {
AntiNameSpoofService service = new AntiNameSpoofService();
service.addProtectedName("Notch");
service.checkPlayerName("N​otch", "10.0.0.5");
service.checkPlayerName("NOTCH", "10.0.0.6");
var status = service.getStatus();
assertTrue((Long) status.get("spoofAttempts") >= 1);
assertTrue((Integer) status.get("blockedNames") >= 1);
}
}
@@ -0,0 +1,728 @@
package com.aluer.security;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* V4.0 反作弊扩展模块测试
* 测试 6 个新增反作弊模块:AntiKillAura, AntiReach, AntiSpeed, AntiJesus, AntiNoFall, AntiScaffold
*
* 每个模块至少3个测试用例,覆盖正常行为、异常行为和 getStatus() 功能
*/
class V40AntiCheatTest {
// ========================================================================
// AntiKillAuraService 测试
// ========================================================================
/**
* 正常攻击行为 — 攻击单一目标,角度有变化,距离在合理范围
*/
@Test
void killAuraNormalAttackReturnsClean() {
AntiKillAuraService svc = new AntiKillAuraService();
Instant now = Instant.now();
// 连续攻击同一个目标
AntiKillAuraService.KillAuraCheckResult r = null;
for (int i = 0; i < 5; i++) {
r = svc.detect("Player1", "uuid-1", "TargetA",
45.0 + i * 10, 10.0 + i * 2,
2.5, now.plusMillis(i * 500));
}
assertNotNull(r);
assertTrue(r.isClean());
assertFalse(r.isFlagged());
assertFalse(r.isSuspicious());
}
/**
* 短时间内攻击过多不同目标 — 应标记为可疑或flagged
*/
@Test
void killAuraRapidTargetSwitchDetected() {
AntiKillAuraService svc = new AntiKillAuraService();
Instant now = Instant.now();
// 在3秒内切换5个不同目标 — 超过MAX_TARGET_SWITCHES(3)
String[] targets = {"A", "B", "C", "D", "E"};
AntiKillAuraService.KillAuraCheckResult lastResult = null;
for (int i = 0; i < targets.length; i++) {
lastResult = svc.detect("Player1", "uuid-1", targets[i],
45.0 + i * 3, 10.0,
2.5, now.plusMillis(i * 200));
}
assertNotNull(lastResult);
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
assertFalse(lastResult.isClean());
}
/**
* 攻击角度高度一致 — 模拟Aimbot(自动瞄准),应标记为flagged
*/
@Test
void killAuraAimbotAngleDetected() {
AntiKillAuraService svc = new AntiKillAuraService();
Instant now = Instant.now();
AntiKillAuraService.KillAuraCheckResult lastResult = null;
// 多次攻击角度几乎不变 — Aimbot特征
for (int i = 0; i < 8; i++) {
lastResult = svc.detect("Player2", "uuid-2", "Target1",
45.0 + (i * 0.5 % 1.0), // yaw几乎不变
10.0 + (i * 0.3 % 0.5), // pitch几乎不变
2.0, now.plusMillis(i * 600));
}
assertNotNull(lastResult);
// 角度一致性应被检测
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
}
/**
* getStatus() 返回非空Map包含必要字段
*/
@Test
void killAuraGetStatusReturnsValidMap() {
AntiKillAuraService svc = new AntiKillAuraService();
svc.detect("P1", "u1", "T1", 45, 10, 2.0, Instant.now());
Map<String, Object> status = svc.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalChecks"));
assertTrue(status.containsKey("flaggedCount"));
assertTrue(status.containsKey("activeTrackedPlayers"));
assertNotNull(status.get("totalChecks"));
assertNotNull(status.get("flaggedCount"));
}
/**
* 完美极限距离模式 — 多次在最大距离范围攻击
*/
@Test
void killAuraPerfectReachPatternDetected() {
AntiKillAuraService svc = new AntiKillAuraService();
Instant now = Instant.now();
AntiKillAuraService.KillAuraCheckResult lastResult = null;
// 多次在2.8-3.0精确距离攻击 — perfect reach
for (int i = 0; i < 12; i++) {
lastResult = svc.detect("Player3", "uuid-3", "TargetX",
30.0 + i * 5, 5.0,
2.9, // 接近极限距离
now.plusMillis(i * 400));
}
assertNotNull(lastResult);
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
}
// ========================================================================
// AntiReachService 测试
// ========================================================================
/**
* 正常攻击距离 — 3.0方块以内
*/
@Test
void reachNormalDistanceReturnsClean() {
AntiReachService svc = new AntiReachService();
Instant now = Instant.now();
AntiReachService.ReachCheckResult r = svc.detect(
"Player1", "uuid-1",
0, 64, 0,
0, 64, 2.5,
2.5, now);
assertNotNull(r);
assertTrue(r.isClean());
assertFalse(r.isFlagged());
}
/**
* 绝对超距攻击 — 超过6.0方块,应标记为flagged
*/
@Test
void reachAbsoluteExceedDetected() {
AntiReachService svc = new AntiReachService();
Instant now = Instant.now();
// 攻击距离超过6.0 — 绝对超距
AntiReachService.ReachCheckResult r = svc.detect(
"Hacker1", "uuid-h1",
0, 64, 0,
0, 64, 7.0,
7.0, now);
assertNotNull(r);
assertTrue(r.isFlagged());
assertTrue(r.getReasons().size() > 0);
}
/**
* 可疑超距 — 超过3.8但不到6.0方块
*/
@Test
void reachSuspiciousDistanceDetected() {
AntiReachService svc = new AntiReachService();
Instant now = Instant.now();
AntiReachService.ReachCheckResult r = svc.detect(
"Player2", "uuid-2",
0, 64, 0,
0, 64, 4.5,
4.5, now);
assertTrue(r.isSuspicious() || r.isFlagged());
}
/**
* 频繁超距攻击 — 多次超过阈值
*/
@Test
void reachFrequentViolationFlagged() {
AntiReachService svc = new AntiReachService();
Instant now = Instant.now();
AntiReachService.ReachCheckResult lastResult = null;
// 多次超过3.8可疑阈值
for (int i = 0; i < 3; i++) {
lastResult = svc.detect("Hacker2", "uuid-h2",
0, 64, 0,
0, 64, 4.0 + i * 0.2,
4.0 + i * 0.2, now.plusMillis(i * 200));
}
assertNotNull(lastResult);
assertFalse(lastResult.isClean());
}
/**
* getStatus() 返回有效状态Map
*/
@Test
void reachGetStatusReturnsValidMap() {
AntiReachService svc = new AntiReachService();
svc.detect("P1", "u1", 0, 64, 0, 0, 64, 2.0, 2.0, Instant.now());
svc.detect("P1", "u1", 0, 64, 0, 0, 64, 7.0, 7.0, Instant.now());
Map<String, Object> status = svc.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalChecks"));
assertTrue(status.containsKey("violations"));
assertTrue(status.containsKey("recentViolations"));
assertNotNull(status.get("totalChecks"));
}
// ========================================================================
// AntiSpeedService 测试
// ========================================================================
/**
* 正常步行速度 — 应返回clean
*/
@Test
void speedNormalWalkingReturnsClean() {
AntiSpeedService svc = new AntiSpeedService();
Instant now = Instant.now();
// 模拟正常步行 ~4.0 m/s
AntiSpeedService.SpeedCheckResult r = svc.detect(
"Player1", "uuid-1",
0, 64, 0,
0.2, 64, 0.2,
true, 0.05, now);
assertNotNull(r);
assertTrue(r.isClean());
}
/**
* 持续超速 — 超过7.0 m/s超过1秒
*/
@Test
void speedSustainedOverspeedFlagged() {
AntiSpeedService svc = new AntiSpeedService();
Instant now = Instant.now();
AntiSpeedService.SpeedCheckResult lastResult = null;
// 持续超速,每次移动0.5方块,间隔50ms = 10 m/s
for (int i = 0; i < 20; i++) {
lastResult = svc.detect("Hacker1", "uuid-h1",
i * 0.5, 64, 0,
(i + 1) * 0.5, 64, 0,
true, 0.05, now.plusMillis(i * 50));
}
assertNotNull(lastResult);
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
}
/**
* GroundSpoof检测 — 声称在地面但垂直速度异常
*/
@Test
void speedGroundSpoofDetected() {
AntiSpeedService svc = new AntiSpeedService();
Instant now = Instant.now();
// onGround=true但Y轴有显著变化
AntiSpeedService.SpeedCheckResult r = svc.detect(
"Hacker2", "uuid-h2",
0, 64, 0,
0.2, 64.5, 0.2,
true, 0.05, now);
assertTrue(r.isSuspicious() || r.isFlagged());
}
/**
* 机械性恒速模式 — 速度恒定
*/
@Test
void speedMechanicalPatternDetected() {
AntiSpeedService svc = new AntiSpeedService();
Instant now = Instant.now();
AntiSpeedService.SpeedCheckResult lastResult = null;
// 每次都以完全相同速度移动 — 机械性Speed hack
for (int i = 0; i < 12; i++) {
lastResult = svc.detect("Hacker3", "uuid-h3",
i * 0.3, 64, 0,
i * 0.3 + 0.3, 64, 0,
true, 0.05, now.plusMillis(i * 50));
}
assertNotNull(lastResult);
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
}
/**
* getStatus() 返回包含统计信息的Map
*/
@Test
void speedGetStatusReturnsValidMap() {
AntiSpeedService svc = new AntiSpeedService();
svc.detect("P1", "u1", 0, 64, 0, 0.2, 64, 0.2, true, 0.05, Instant.now());
svc.detect("P1", "u1", 0.2, 64, 0.2, 0.4, 64, 0.4, true, 0.05, Instant.now());
Map<String, Object> status = svc.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalSamples"));
assertTrue(status.containsKey("speedViolations"));
assertTrue(status.containsKey("averageSpeed"));
assertTrue(status.containsKey("maxSpeed"));
}
// ========================================================================
// AntiJesusService 测试
// ========================================================================
/**
* 正常在水中游泳 — 不在液体表面行走
*/
@Test
void jesusNormalSwimmingReturnsClean() {
AntiJesusService svc = new AntiJesusService();
Instant now = Instant.now();
// 玩家在水中(Y=61.5, 水面Y=63)— 正在下沉/游泳中
AntiJesusService.JesusCheckResult r = svc.detect(
"Player1", "uuid-1",
61.5, 63.0,
"WATER", false, true, now);
assertNotNull(r);
assertTrue(r.isClean());
}
/**
* 水上行走 — 站在水面高度且声称在地面
*/
@Test
void jesusWalkingOnWaterSuspicious() {
AntiJesusService svc = new AntiJesusService();
Instant now = Instant.now();
AntiJesusService.JesusCheckResult lastResult = null;
// 连续站在水面上 — Jesus hack特征
for (int i = 0; i < 5; i++) {
lastResult = svc.detect("Hacker1", "uuid-h1",
63.1, 63.0,
"WATER", true, true, now.plusMillis(i * 500));
}
assertNotNull(lastResult);
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
}
/**
* 持续水上行走超过1秒+抖动模式 — 双规则命中应标记为flagged
*/
@Test
void jesusSustainedWaterWalkingFlagged() {
AntiJesusService svc = new AntiJesusService();
Instant now = Instant.now();
AntiJesusService.JesusCheckResult lastResult = null;
// 持续站在水面上超过1秒 + 抖动振荡(触发双规则)
double[] yValues = {63.0, 63.05, 62.95, 63.02, 62.98, 63.03, 62.97, 63.01, 63.04, 62.96};
for (int i = 0; i < yValues.length; i++) {
lastResult = svc.detect("Hacker2", "uuid-h2",
yValues[i], 63.0,
"WATER", true, true, now.plusMillis(i * 200));
}
assertNotNull(lastResult);
assertTrue(lastResult.isFlagged());
assertTrue(lastResult.getReasons().stream()
.anyMatch(r -> r.contains("SUSTAINED_JESUS")));
}
/**
* 抖动模式检测 — Y坐标在水面附近振荡
*/
@Test
void jesusJitterPatternDetected() {
AntiJesusService svc = new AntiJesusService();
Instant now = Instant.now();
AntiJesusService.JesusCheckResult lastResult = null;
// 模拟抖动:Y值在水面附近反复小幅变化
double[] yValues = {63.0, 63.05, 62.95, 63.02, 62.98, 63.03, 62.97, 63.01};
for (int i = 0; i < yValues.length; i++) {
lastResult = svc.detect("Hacker3", "uuid-h3",
yValues[i], 63.0,
"WATER", true, true, now.plusMillis(i * 300));
}
assertNotNull(lastResult);
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
}
/**
* getStatus() 返回有效状态Map
*/
@Test
void jesusGetStatusReturnsValidMap() {
AntiJesusService svc = new AntiJesusService();
svc.detect("P1", "u1", 62.0, 63.0, "WATER", false, true, Instant.now());
Map<String, Object> status = svc.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalChecks"));
assertTrue(status.containsKey("jesusViolations"));
assertTrue(status.containsKey("activeTrackedPlayers"));
}
// ========================================================================
// AntiNoFallService 测试
// ========================================================================
/**
* 正常坠落并受伤害 — 应返回clean
*/
@Test
void noFallNormalFallWithDamageReturnsClean() {
AntiNoFallService svc = new AntiNoFallService();
Instant now = Instant.now();
// 记录从Y=80坠落
svc.recordFallStart("Player1", "uuid-1", 80.0, "survival", now);
// 著陆在Y=70,受到伤害
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
"Player1", "uuid-1", 70.0,
true, 5.0, // tookDamage=true
false, false, false, false, false, false, // 无合法无伤害减免
now.plusSeconds(2));
assertNotNull(r);
assertTrue(r.isClean());
}
/**
* 短距离坠落不受伤 — 正常行为
*/
@Test
void noFallShortFallNoDamageNormal() {
AntiNoFallService svc = new AntiNoFallService();
Instant now = Instant.now();
// 坠落不足3方块 — 不会产生伤害
svc.recordFallStart("Player2", "uuid-2", 66.0, "survival", now);
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
"Player2", "uuid-2", 65.0,
false, 0,
false, false, false, false, false, false,
now.plusMillis(500));
assertNotNull(r);
assertTrue(r.isClean());
}
/**
* 高空坠落无伤害 — NoFall hack特征
*/
@Test
void noFallHighFallNoDamageFlagged() {
AntiNoFallService svc = new AntiNoFallService();
Instant now = Instant.now();
// 从12方块高度坠落但不受伤 — 无合法理由
svc.recordFallStart("Hacker1", "uuid-h1", 82.0, "survival", now);
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
"Hacker1", "uuid-h1", 70.0,
false, 0, // 没有受到伤害
false, false, false, false, false, false,
now.plusSeconds(3));
assertNotNull(r);
assertTrue(r.isSuspicious() || r.isFlagged());
}
/**
* 合法无伤害 — 落入水中
*/
@Test
void noFallWaterLandingNoDamageIsLegitimate() {
AntiNoFallService svc = new AntiNoFallService();
Instant now = Instant.now();
// 掉入水中 — 合法免疫摔落伤害
svc.recordFallStart("Player3", "uuid-3", 80.0, "survival", now);
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
"Player3", "uuid-3", 62.0,
false, 0,
true, // isInLiquid=true (水中)
false, false, false, false, false,
now.plusSeconds(2));
// 落入水中是合法的无伤害情况,不应标记
assertNotNull(r);
assertTrue(r.isClean());
}
/**
* 合法无伤害 — 鞘翅飞行
*/
@Test
void noFallElytraFlyingIsLegitimate() {
AntiNoFallService svc = new AntiNoFallService();
Instant now = Instant.now();
// 鞘翅飞行期间不受伤 — 合法
svc.recordFallStart("Player4", "uuid-4", 100.0, "survival", now);
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
"Player4", "uuid-4", 70.0,
false, 0,
false, false, false, false, false,
true, // isElytraFlying=true
now.plusSeconds(5));
assertNotNull(r);
assertTrue(r.isClean());
}
/**
* 粘液块上无伤害 — 合法
*/
@Test
void noFallSlimeBlockLandingIsLegitimate() {
AntiNoFallService svc = new AntiNoFallService();
Instant now = Instant.now();
// 落在粘液块上 — 合法免疫摔落伤害
svc.recordFallStart("Player5", "uuid-5", 80.0, "survival", now);
AntiNoFallService.NoFallCheckResult r = svc.recordLanding(
"Player5", "uuid-5", 60.0,
false, 0,
false, true, // isOnSlimeBlock=true
false, false, false, false,
now.plusSeconds(2));
assertNotNull(r);
assertTrue(r.isClean());
}
/**
* getStatus() 返回有效状态Map
*/
@Test
void noFallGetStatusReturnsValidMap() {
AntiNoFallService svc = new AntiNoFallService();
Instant now = Instant.now();
svc.recordFallStart("P1", "u1", 80.0, "survival", now);
svc.recordLanding("P1", "u1", 70.0, true, 5.0,
false, false, false, false, false, false, now.plusSeconds(2));
Map<String, Object> status = svc.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalFalls"));
assertTrue(status.containsKey("noFallViolations"));
assertTrue(status.containsKey("falsePositives"));
}
// ========================================================================
// AntiScaffoldService 测试
// ========================================================================
/**
* 正常方块放置 — 间隔合理,角度正常
*/
@Test
void scaffoldNormalPlacementReturnsClean() {
AntiScaffoldService svc = new AntiScaffoldService();
Instant now = Instant.now();
AntiScaffoldService.ScaffoldCheckResult r = svc.detect(
"Player1", "uuid-1",
10, 63, 10,
180, 30, // yaw=180, pitch=30(正常视角)
false, true, "COBBLESTONE",
now);
assertNotNull(r);
assertTrue(r.isClean());
}
/**
* 高速连续放置 — 超过5方块/秒持续3秒
*/
@Test
void scaffoldRapidPlacementFlagged() {
AntiScaffoldService svc = new AntiScaffoldService();
Instant now = Instant.now();
AntiScaffoldService.ScaffoldCheckResult lastResult = null;
// 高速放置方块:每次间隔100ms = 10方块/秒
for (int i = 0; i < 30; i++) {
lastResult = svc.detect("Hacker1", "uuid-h1",
10 + i, 63, 10,
90, -85, // pitch=-85 (近乎垂直向下看)
true, true, "COBBLESTONE",
now.plusMillis(i * 100));
}
assertNotNull(lastResult);
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
}
/**
* Scaffold角度检测 — 向下看并精确放置
*/
@Test
void scaffoldDownwardAngleAndPlacementPattern() {
AntiScaffoldService svc = new AntiScaffoldService();
Instant now = Instant.now();
AntiScaffoldService.ScaffoldCheckResult lastResult = null;
// 向下看 (pitch接近-90) + 脚下放置 — 典型Scaffold
for (int i = 0; i < 10; i++) {
lastResult = svc.detect("Hacker2", "uuid-h2",
10 + i, 63, 10,
0, -88, // 几乎是正下方
true, true, "COBBLESTONE",
now.plusMillis(i * 150));
}
assertNotNull(lastResult);
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
}
/**
* Safewalk检测 — 连续边缘放置
*/
@Test
void scaffoldSafewalkPattern() {
AntiScaffoldService svc = new AntiScaffoldService();
Instant now = Instant.now();
AntiScaffoldService.ScaffoldCheckResult lastResult = null;
// 大量在脚下放置方块(Safewalk行为)
for (int i = 0; i < 20; i++) {
lastResult = svc.detect("Hacker3", "uuid-h3",
10 + i, 63, 10,
90, -75,
true, // isPlaceBelowPlayer=true(脚下放置)
false, "COBBLESTONE",
now.plusMillis(i * 80));
}
assertNotNull(lastResult);
assertTrue(lastResult.isSuspicious() || lastResult.isFlagged());
}
/**
* getStatus() 返回有效状态Map
*/
@Test
void scaffoldGetStatusReturnsValidMap() {
AntiScaffoldService svc = new AntiScaffoldService();
svc.detect("P1", "u1", 10, 63, 10, 180, 30,
false, true, "STONE", Instant.now());
svc.detect("P1", "u1", 11, 63, 10, 180, 32,
false, true, "STONE", Instant.now());
Map<String, Object> status = svc.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalPlacements"));
assertTrue(status.containsKey("scaffoldViolations"));
assertTrue(status.containsKey("trackedBuilders"));
}
// ========================================================================
// 模块关闭验证测试 — 通过直接创建带无参构造的服务实例测试
// 无参构造函数使用 new ServerGuardConfig(),默认所有开关为true
// 此处验证配置默认为true时服务正常执行检测
// ========================================================================
/**
* All services instantiable and return clean for trivial inputs
*/
@Test
void allServicesInstantiableAndFunctional() {
// 验证所有服务可通过无参构造实例化
AntiKillAuraService killAura = new AntiKillAuraService();
AntiReachService reach = new AntiReachService();
AntiSpeedService speed = new AntiSpeedService();
AntiJesusService jesus = new AntiJesusService();
AntiNoFallService noFall = new AntiNoFallService();
AntiScaffoldService scaffold = new AntiScaffoldService();
assertNotNull(killAura);
assertNotNull(reach);
assertNotNull(speed);
assertNotNull(jesus);
assertNotNull(noFall);
assertNotNull(scaffold);
// 验证所有服务的 getStatus() 都可调用
assertNotNull(killAura.getStatus());
assertNotNull(reach.getStatus());
assertNotNull(speed.getStatus());
assertNotNull(jesus.getStatus());
assertNotNull(noFall.getStatus());
assertNotNull(scaffold.getStatus());
}
}
@@ -0,0 +1,361 @@
package com.aluer.security;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class V40ChatSecurityTest {
// ================================================================
// ChatFloodProtectionService Tests
// ================================================================
@Test
void chatFloodDetectsShortWindowFlood() {
ChatFloodProtectionService cf = new ChatFloodProtectionService();
// 3秒窗口内快速发送6条消息应触发洪水检测
for (int i = 0; i < 6; i++) {
cf.check("Player1", "Test message " + i);
}
// 第7条应该被拦截
ChatFloodProtectionService.CheckResult r = cf.check("Player1", "Another message");
assertTrue(r.isBlocked(), "6条消息在3秒窗口内应触发洪水检测");
assertFalse(r.getReasons().isEmpty());
}
@Test
void chatFloodAllowsNormalChatRate() {
ChatFloodProtectionService cf = new ChatFloodProtectionService();
// 发送2条消息不应触发洪水
ChatFloodProtectionService.CheckResult r1 = cf.check("Player2", "Hello");
assertTrue(r1.isClean());
ChatFloodProtectionService.CheckResult r2 = cf.check("Player2", "How are you?");
assertTrue(r2.isClean());
// 验证计数器运作
Map<String, Object> status = cf.getStatus();
assertTrue((Long) status.get("totalMessages") >= 2);
}
@Test
void chatFloodDetectsUnicodeFlood() {
ChatFloodProtectionService cf = new ChatFloodProtectionService();
// 构建超过50%为CJK特殊字符的长消息
StringBuilder unicodeMsg = new StringBuilder();
for (int i = 0; i < 50; i++) {
unicodeMsg.append("测试测试测试测试"); // CJK字符
}
ChatFloodProtectionService.CheckResult r = cf.check("Player3", unicodeMsg.toString());
assertTrue(r.isBlocked(), "超过50%CJK字符应触发Unicode洪水检测");
}
@Test
void chatFloodDetectsSimilarMessages() {
ChatFloodProtectionService cf = new ChatFloodProtectionService();
// 连续3条高度相似的消息(编辑距离<3)
cf.check("Player4", "Selling cheap items");
cf.check("Player4", "Selling cheap items!"); // 只差1个字符
ChatFloodProtectionService.CheckResult r = cf.check("Player4", "Selling cheap items!!"); // 只差2个字符
assertTrue(r.isBlocked(), "连续3条高度相似的消息应触发重复洪水检测");
}
@Test
void chatFloodStatusContainsExpectedFields() {
ChatFloodProtectionService cf = new ChatFloodProtectionService();
cf.check("Player5", "Message one");
cf.check("Player5", "Message two");
Map<String, Object> status = cf.getStatus();
assertNotNull(status.get("totalMessages"));
assertNotNull(status.get("mutedPlayers"));
assertNotNull(status.get("floodEvents"));
assertNotNull(status.get("activeTrackers"));
assertTrue(status.get("enabled") instanceof Boolean);
}
// ================================================================
// AntiAdvertisementService Tests
// ================================================================
@Test
void antiAdDetectsIpAddress() {
AntiAdvertisementService ad = new AntiAdvertisementService();
AntiAdvertisementService.CheckResult r = ad.check("Player1",
"Join my server at 123.45.67.89:25565!");
assertTrue(r.isBlocked(), "消息中包含IP地址应被检测为广告");
}
@Test
void antiAdDetectsDomain() {
AntiAdvertisementService ad = new AntiAdvertisementService();
AntiAdvertisementService.CheckResult r = ad.check("Player2",
"Check out myserver.com for great gameplay!");
assertTrue(r.isBlocked(), "消息中包含.com域名应被检测为广告");
}
@Test
void antiAdDetectsDiscordInvite() {
AntiAdvertisementService ad = new AntiAdvertisementService();
AntiAdvertisementService.CheckResult r = ad.check("Player3",
"Join our discord.gg/abcdef for giveaways!");
assertTrue(r.isBlocked(), "消息中包含Discord邀请应被检测为广告");
}
@Test
void antiAdDetectsQQGroup() {
AntiAdvertisementService ad = new AntiAdvertisementService();
AntiAdvertisementService.CheckResult r = ad.check("Player4",
"加群一起玩:12345678");
assertTrue(r.isBlocked(), "消息中包含QQ群号应被检测为广告");
}
@Test
void antiAdAllowsNormalMessage() {
AntiAdvertisementService ad = new AntiAdvertisementService();
AntiAdvertisementService.CheckResult r = ad.check("Player5",
"Hey guys, how is everyone doing today?");
assertTrue(r.isClean(), "正常聊天消息不应被拦截");
}
@Test
void antiAdWhitelistIpPasses() {
AntiAdvertisementService ad = new AntiAdvertisementService();
ad.addWhitelistedIp("192.168.1.1");
AntiAdvertisementService.CheckResult r = ad.check("Player6",
"Server IP is 192.168.1.1:25565");
assertTrue(r.isClean(), "白名单IP不应被拦截");
}
@Test
void antiAdStatusHasCounters() {
AntiAdvertisementService ad = new AntiAdvertisementService();
ad.check("Player7", "Join 10.0.0.1");
Map<String, Object> status = ad.getStatus();
assertTrue((Long) status.get("totalMessages") >= 1);
assertNotNull(status.get("adsDetected"));
assertNotNull(status.get("blockedMessages"));
}
// ================================================================
// AntiPhishingLinkService Tests
// ================================================================
@Test
void antiPhishDetectsKnownPhishingDomain() {
AntiPhishingLinkService ap = new AntiPhishingLinkService();
AntiPhishingLinkService.CheckResult r = ap.check("Player1",
"Verify your account at http://mc-login.com/verify");
assertTrue(r.isBlocked(), "已知钓鱼域名应被检测");
}
@Test
void antiPhishDetectsFakeMojangDomain() {
AntiPhishingLinkService ap = new AntiPhishingLinkService();
AntiPhishingLinkService.CheckResult r = ap.check("Player2",
"Free cape at http://minecraft-login.net/free");
assertTrue(r.isBlocked(), "包含minecraft关键字的非官方域名应被检测");
}
@Test
void antiPhishDetectsShortUrl() {
AntiPhishingLinkService ap = new AntiPhishingLinkService();
AntiPhishingLinkService.CheckResult r = ap.check("Player3",
"Claim your reward: http://bit.ly/freemc123");
assertTrue(r.isBlocked(), "短链接应被标记");
}
@Test
void antiPhishDetectsFreeMcScam() {
AntiPhishingLinkService ap = new AntiPhishingLinkService();
AntiPhishingLinkService.CheckResult r = ap.check("Player4",
"Get free minecraft account at http://steal-account.info");
assertTrue(r.isBlocked(), "免费Minecraft骗局应被检测");
}
@Test
void antiPhishDetectsMismatchedText() {
AntiPhishingLinkService ap = new AntiPhishingLinkService();
AntiPhishingLinkService.CheckResult r = ap.check("Player5",
"Mojang官方验证请访问: http://phish-site.com/login");
assertTrue(r.isBlocked(), "声称官方但链接非官方应被检测为文本链接不匹配");
}
@Test
void antiPhishAllowsNormalMessage() {
AntiPhishingLinkService ap = new AntiPhishingLinkService();
AntiPhishingLinkService.CheckResult r = ap.check("Player6",
"Hey everyone! Let's build something cool today.");
assertTrue(r.isClean(), "不含URL的正常消息不应被拦截");
}
@Test
void antiPhishStatusHasCounters() {
AntiPhishingLinkService ap = new AntiPhishingLinkService();
ap.check("Player7", "Visit http://mc-login.com");
Map<String, Object> status = ap.getStatus();
assertNotNull(status.get("totalUrls"));
assertNotNull(status.get("phishingDetected"));
assertNotNull(status.get("shortUrlsResolved"));
}
// ================================================================
// AntiCommandAbuseService Tests
// ================================================================
@Test
void antiCmdDetectsCommandFlood() {
AntiCommandAbuseService ac = new AntiCommandAbuseService();
// 10秒窗口内快速发送12条命令应触发洪水检测
for (int i = 0; i < 12; i++) {
ac.check("Player1", "/tp " + i, "tp");
}
// 第13条应被拦截
AntiCommandAbuseService.CheckResult r = ac.check("Player1", "/tp 13", "tp");
assertTrue(r.isBlocked(), "10秒内超过10条命令应触发洪水检测");
}
@Test
void antiCmdDetectsShellInjection() {
AntiCommandAbuseService ac = new AntiCommandAbuseService();
AntiCommandAbuseService.CheckResult r = ac.check("Player2",
"/say hello; rm -rf /", "say", "; rm -rf /", true);
assertTrue(r.isBlocked(), "包含Shell注入字符的命令应被检测");
}
@Test
void antiCmdDetectsPluginScanning() {
AntiCommandAbuseService ac = new AntiCommandAbuseService();
// 连续执行不存在的命令(模拟插件扫描)
for (int i = 0; i < 6; i++) {
ac.check("Player3", "/nonexistent" + i, "nonexistent" + i, "", false);
}
AntiCommandAbuseService.CheckResult r = ac.check("Player3",
"/nonexistent6", "nonexistent6", "", false);
assertTrue(r.isBlocked(), "连续执行不存在命令应被检测为插件扫描");
}
@Test
void antiCmdAllowsNormalCommands() {
AntiCommandAbuseService ac = new AntiCommandAbuseService();
AntiCommandAbuseService.CheckResult r = ac.check("Player4",
"/home", "home", null, true);
assertTrue(r.isClean(), "正常命令不应被拦截");
}
@Test
void antiCmdDetectsDollarSubCommandInjection() {
AntiCommandAbuseService ac = new AntiCommandAbuseService();
AntiCommandAbuseService.CheckResult r = ac.check("Player5",
"/say $(cat /etc/passwd)", "say", "$(cat /etc/passwd)", true);
assertTrue(r.isBlocked(), "$() Shell注入应被检测");
}
@Test
void antiCmdStatusHasCommandStats() {
AntiCommandAbuseService ac = new AntiCommandAbuseService();
ac.check("Player6", "/home", "home");
ac.check("Player6", "/home", "home");
ac.check("Player6", "/spawn", "spawn");
Map<String, Object> status = ac.getStatus();
assertNotNull(status.get("totalCommands"));
assertNotNull(status.get("blockedCommands"));
assertNotNull(status.get("commandStats"));
}
// ================================================================
// PlayerPrivacyService Tests
// ================================================================
@Test
void privacyAnonymizesIpAddress() {
PlayerPrivacyService pp = new PlayerPrivacyService();
String result = pp.anonymizeIp("Player connected from 192.168.1.100");
assertTrue(result.contains("xxx.xxx"), "IP地址应被脱敏为 xxx.xxx");
assertTrue(result.contains("192.168"), "IP前两位应保留");
assertFalse(result.contains("1.100"), "IP后两位应被隐藏");
}
@Test
void privacyAnonymizesMultipleIps() {
PlayerPrivacyService pp = new PlayerPrivacyService();
String result = pp.anonymizeIp("Connections: 10.0.0.1 and 172.16.5.42");
assertTrue(result.contains("10.0.xxx.xxx"), "第一个IP应被脱敏");
assertTrue(result.contains("172.16.xxx.xxx"), "第二个IP应被脱敏");
}
@Test
void privacyRegionLevelRoundsCoordinates() {
PlayerPrivacyService pp = new PlayerPrivacyService();
pp.setCoordinateAnonymizationLevel(
PlayerPrivacyService.CoordinateAnonymizationLevel.REGION);
String result = pp.anonymizeCoordinates(128.5, 64.1, -256.8);
// 128.5 -> 130, 64.1 -> 60, -256.8 -> -260
assertTrue(result.contains("130"), "X坐标应四舍五入到10");
assertTrue(result.contains("60"), "Y坐标应四舍五入到10");
assertTrue(result.contains("-260"), "Z坐标应四舍五入到10");
}
@Test
void privacyHiddenLevelHidesCoordinates() {
PlayerPrivacyService pp = new PlayerPrivacyService();
pp.setCoordinateAnonymizationLevel(
PlayerPrivacyService.CoordinateAnonymizationLevel.HIDDEN);
String result = pp.anonymizeCoordinates(100, 64, 200);
assertEquals("[***, ***, ***]", result, "HIDDEN级别应完全隐藏坐标");
}
@Test
void privacyDataExportRequest() {
PlayerPrivacyService pp = new PlayerPrivacyService();
String requestId = pp.requestDataExport("Player1", "admin");
assertNotNull(requestId, "导出请求应返回有效ID");
List<PlayerPrivacyService.DataExportRequest> requests = pp.getPendingRequests();
assertEquals(1, requests.size());
assertEquals("Player1", requests.get(0).getPlayerName());
assertEquals("EXPORT", requests.get(0).getType());
}
@Test
void privacyDataDeletionRequest() {
PlayerPrivacyService pp = new PlayerPrivacyService();
String requestId = pp.requestDataDeletion("Player2", "player");
assertNotNull(requestId, "删除请求应返回有效ID");
Map<String, Object> status = pp.getStatus();
assertTrue(((Number) status.get("pendingDataRequests")).intValue() >= 1);
}
@Test
void privacyAuditRecordsAccess() {
PlayerPrivacyService pp = new PlayerPrivacyService();
pp.auditAccess("dashboard-api", "Player3", "IP_ADDRESS");
pp.auditAccess("dashboard-api", "Player3", "COORDINATES");
List<PlayerPrivacyService.AuditEntry> audits = pp.getAuditForPlayer("Player3");
assertEquals(2, audits.size(), "应记录2条审计日志");
assertEquals("IP_ADDRESS", audits.get(0).getDataType());
assertEquals("COORDINATES", audits.get(1).getDataType());
}
@Test
void privacyPrefersNonNullAudit() {
PlayerPrivacyService pp = new PlayerPrivacyService();
List<PlayerPrivacyService.AuditEntry> audits = pp.getAuditForPlayer("NonExistent");
assertNotNull(audits, "不存在的玩家应返回空列表而非null");
assertTrue(audits.isEmpty());
}
@Test
void privacyStatusHasExpectedFields() {
PlayerPrivacyService pp = new PlayerPrivacyService();
pp.anonymizeIp("Player from 1.2.3.4");
Map<String, Object> status = pp.getStatus();
assertNotNull(status.get("anonymizedEntries"));
assertNotNull(status.get("pendingDataRequests"));
assertNotNull(status.get("privacyViolations"));
assertNotNull(status.get("coordinateLevel"));
assertNotNull(status.get("retentionDays"));
}
}
@@ -0,0 +1,533 @@
package com.aluer.security;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* V4.0 玩家行为安全模块测试 —— 每个模块至少 3 个测试(正常/异常/状态)。
*
* 覆盖的模块:
* 1. AntiNukerService — 快速破坏检测
* 2. AntiAutoClickerService — 自动点击检测
* 3. AntiChestStealService — 自动偷箱检测
* 4. AntiAutoFishService — 自动钓鱼检测
* 5. AntiInventoryManipulationService — 背包操作异常检测
* 6. AntiBaritoneService — AI 寻路机器人检测
*/
class V40PlayerBehaviorTest {
// ==================== AntiNukerService Tests ====================
/**
* 正常玩家的方块破坏行为应返回 clean。
* 正常挖掘速度约 0.3-0.4 秒/块,500ms 内破坏 1-2 个方块是合理的。
*/
@Test
void antiNukerNormalBreakingReturnsClean() {
AntiNukerService service = new AntiNukerService();
// 在 500ms 间隔内只破坏 1 个方块(正常挖矿节奏)
AntiNukerService.DetectionResult r = service.detect(
"NormalPlayer", "uuid-001", "STONE", 10, 64, 10, "world");
assertTrue(r.isClean(), "Normal breaking should be clean");
assertFalse(r.isFlagged());
}
/**
* 快速破坏模式(Nuker)应被检测为异常。
* 在 500ms 窗口内破坏超过 3 个方块属于 Nuker 行为。
*/
@Test
void antiNukerRapidBreakingDetectsNuker() {
AntiNukerService service = new AntiNukerService();
// 模拟 500ms 内破坏 6 个方块(Nuker 特征)
for (int i = 0; i < 6; i++) {
service.detect("NukerBot", "uuid-002", "STONE",
i, 64, i, "world");
}
long violations = service.getNukerViolations();
assertTrue(violations > 0 || service.getTotalBreaks() >= 6,
"Rapid breaking should be detected as nuker violation");
}
/**
* 同 tick 破坏多块(多目标同时破坏)应被标记为 flagged。
* 这是 Nuker 的终极特征——只有发包修改才能在同一 tick 破坏多个方块。
*/
@Test
void antiNukerMultiTargetSameTickIsFlagged() {
AntiNukerService service = new AntiNukerService();
// 在极短时间内破坏 3 个方块,模拟同一 tick 多目标破坏
service.detect("TickHacker", "uuid-003", "STONE", 0, 64, 0, "world");
service.detect("TickHacker", "uuid-003", "STONE", 1, 64, 1, "world");
service.detect("TickHacker", "uuid-003", "STONE", 2, 64, 2, "world");
AntiNukerService.DetectionResult r = service.detect(
"TickHacker", "uuid-003", "STONE", 3, 64, 3, "world");
assertTrue(r.isFlagged() || r.isSuspicious(),
"Multiple blocks in same tick should be suspicious or flagged");
}
/**
* 分散破坏模式——大范围破坏应被检测。
* Nuker 在大范围同时破坏,正常玩家挖掘范围小。
*/
@Test
void antiNukerScatteredBreakingDetected() {
AntiNukerService service = new AntiNukerService();
// 在大范围内快速破坏方块
service.detect("ScatterBot", "uuid-004", "STONE", 0, 64, 0, "world");
service.detect("ScatterBot", "uuid-004", "STONE", 10, 64, 0, "world");
service.detect("ScatterBot", "uuid-004", "STONE", 0, 64, 10, "world");
service.detect("ScatterBot", "uuid-004", "STONE", 10, 64, 10, "world");
AntiNukerService.DetectionResult r = service.detect(
"ScatterBot", "uuid-004", "STONE", 20, 64, 20, "world");
// 分散模式可能会触发 suspicious
assertNotNull(r);
}
/**
* getStatus() 应返回包含关键指标的 LinkedHashMap。
*/
@Test
void antiNukerStatusReturnsExpectedFields() {
AntiNukerService service = new AntiNukerService();
service.detect("StatusTest", "uuid-005", "STONE", 0, 64, 0, "world");
service.detect("StatusTest", "uuid-005", "STONE", 1, 64, 1, "world");
Map<String, Object> status = service.getStatus();
assertNotNull(status, "Status should not be null");
assertTrue(status.containsKey("totalBreaks"), "Should contain totalBreaks");
assertTrue(status.containsKey("nukerViolations"), "Should contain nukerViolations");
assertTrue(status.containsKey("trackedPlayers"), "Should contain trackedPlayers");
assertTrue(status.containsKey("flaggedPlayers"), "Should contain flaggedPlayers");
assertTrue(status.containsKey("enabled"), "Should contain enabled");
assertTrue(status.get("totalBreaks") instanceof Long);
assertTrue((Long) status.get("totalBreaks") >= 2);
}
// ==================== AntiAutoClickerService Tests ====================
/**
* 正常点击频率应返回 clean(CPS < 15)。
*/
@Test
void antiAutoClickerNormalCpsReturnsClean() {
AntiAutoClickerService service = new AntiAutoClickerService();
// 正常 CPS:在 1 秒内有 10 次点击(正常上限内)
for (int i = 0; i < 10; i++) {
AntiAutoClickerService.DetectionResult r = service.detect(
"NormalPlayer", "uuid-010", "LEFT_CLICK");
// 前几个点击可能因样本不足返回 clean
if (i >= 9) {
assertTrue(r.isClean() || r.isSuspicious());
assertFalse(r.isFlagged());
}
}
}
/**
* 超高 CPS(> 20)应被检测为异常。
*/
@Test
void antiAutoClickerHighCpsDetected() {
AntiAutoClickerService service = new AntiAutoClickerService();
// 在短时间内大量点击模拟高 CPS
for (int i = 0; i < 30; i++) {
service.detect("ClickerBot", "uuid-011", "LEFT_CLICK");
}
long highCps = service.getHighCpsCount();
assertTrue(highCps > 0 || service.getMacroSuspected() > 0,
"High CPS should be detected");
}
/**
* 均匀点击间隔(低熵)应被检测为宏行为。
*/
@Test
void antiAutoClickerUniformIntervalsDetectedAsMacro() {
AntiAutoClickerService service = new AntiAutoClickerService();
// 模拟均匀间隔的点击(宏特征)
for (int i = 0; i < 25; i++) {
service.detect("MacroBot", "uuid-012", "LEFT_CLICK");
}
// 至少应有高 CPS 检测或宏疑似
assertTrue(service.getTotalPlayers() >= 1);
}
/**
* getStatus() 应包含 totalPlayers、highCpsCount、macroSuspected。
*/
@Test
void antiAutoClickerStatusReturnsExpectedFields() {
AntiAutoClickerService service = new AntiAutoClickerService();
service.detect("StatusPlayer", "uuid-013", "LEFT_CLICK");
Map<String, Object> status = service.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalPlayers"), "Should contain totalPlayers");
assertTrue(status.containsKey("highCpsCount"), "Should contain highCpsCount");
assertTrue(status.containsKey("macroSuspected"), "Should contain macroSuspected");
assertTrue(status.containsKey("trackedPlayers"), "Should contain trackedPlayers");
}
// ==================== AntiChestStealService Tests ====================
/**
* 正常打开箱子应返回 clean。
*/
@Test
void antiChestStealNormalOpenReturnsClean() {
AntiChestStealService service = new AntiChestStealService();
AntiChestStealService.DetectionResult r = service.detectOpen(
"NormalPlayer", "uuid-020", "CHEST", 100, 64, 100);
assertTrue(r.isClean(), "Normal chest open should be clean");
assertFalse(r.isFlagged());
}
/**
* 瞬间取走大量物品应被检测为 Chest Stealer。
*/
@Test
void antiChestStealInstantLootDetected() {
AntiChestStealService service = new AntiChestStealService();
// 先记录打开事件
service.detectOpen("Stealer", "uuid-021", "CHEST", 100, 64, 100);
// 在打开后立即取走大量物品
for (int i = 0; i < 8; i++) {
service.detectTakeItem("Stealer", "uuid-021",
"DIAMOND", 1, "CHEST", i);
}
long instantLoot = service.getInstantLootCount();
long violations = service.getStealViolations();
assertTrue(instantLoot > 0 || violations > 0,
"Instant loot should be detected");
}
/**
* 高价值物品快速取走应被检测。
*/
@Test
void antiChestStealHighValueFastTakeDetected() {
AntiChestStealService service = new AntiChestStealService();
service.detectOpen("Valuesteal", "uuid-022", "CHEST", 100, 64, 100);
// 连续取走高价值物品
AntiChestStealService.DetectionResult r1 = service.detectTakeItem(
"Valuesteal", "uuid-022", "DIAMOND", 1, "CHEST", 0);
AntiChestStealService.DetectionResult r2 = service.detectTakeItem(
"Valuesteal", "uuid-022", "NETHERITE_INGOT", 1, "CHEST", 1);
AntiChestStealService.DetectionResult r3 = service.detectTakeItem(
"Valuesteal", "uuid-022", "EMERALD", 1, "CHEST", 2);
AntiChestStealService.DetectionResult r4 = service.detectTakeItem(
"Valuesteal", "uuid-022", "TOTEM_OF_UNDYING", 1, "CHEST", 3);
AntiChestStealService.DetectionResult r5 = service.detectTakeItem(
"Valuesteal", "uuid-022", "ELYTRA", 1, "CHEST", 4);
// 至少部分检测应返回 suspicious 或 flagged
assertNotNull(r1);
assertNotNull(r5);
}
/**
* getStatus() 应包含 totalChestOpens、stealViolations、instantLootCount。
*/
@Test
void antiChestStealStatusReturnsExpectedFields() {
AntiChestStealService service = new AntiChestStealService();
service.detectOpen("StatusPlayer", "uuid-023", "CHEST", 0, 64, 0);
Map<String, Object> status = service.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalChestOpens"), "Should contain totalChestOpens");
assertTrue(status.containsKey("stealViolations"), "Should contain stealViolations");
assertTrue(status.containsKey("instantLootCount"), "Should contain instantLootCount");
assertTrue(status.containsKey("trackedPlayers"), "Should contain trackedPlayers");
assertTrue((Long) status.get("totalChestOpens") >= 1);
}
// ==================== AntiAutoFishService Tests ====================
/**
* 正常钓鱼抛竿应被记录。
*/
@Test
void antiAutoFishNormalCastRecorded() {
AntiAutoFishService service = new AntiAutoFishService();
service.recordCast("NormalFisher", "uuid-030", 100, 64, 100);
assertTrue(service.getTotalCastings() >= 1,
"Casting should be recorded");
}
/**
* 连续极快收竿应被检测为自动钓鱼。
*/
@Test
void antiAutoFishConsecutiveFastReelDetected() {
AntiAutoFishService service = new AntiAutoFishService();
// 模拟多次快速抛竿和收竿
for (int i = 0; i < 5; i++) {
service.recordCast("AutoFisher", "uuid-031", 100, 64, 100);
service.detectReel("AutoFisher", "uuid-031", true, 0, 0);
}
long violations = service.getAutoFishViolations();
// 多次连续快速收竿可能被检测
assertTrue(service.getTotalCastings() >= 5);
}
/**
* 正常玩家单次收竿返回 clean。
*/
@Test
void antiAutoFishNormalReelReturnsClean() {
AntiAutoFishService service = new AntiAutoFishService();
service.recordCast("CasualFisher", "uuid-032", 50, 64, 50);
AntiAutoFishService.DetectionResult r = service.detectReel(
"CasualFisher", "uuid-032", false, 0, 0);
// 单次收竿(首次)应返回 clean 或 suspicious(样本不足)
assertNotNull(r);
}
/**
* getStatus() 应包含 totalCastings、autoFishViolations、reactionTimeStats。
*/
@Test
void antiAutoFishStatusReturnsExpectedFields() {
AntiAutoFishService service = new AntiAutoFishService();
service.recordCast("StatusFisher", "uuid-033", 100, 64, 100);
service.detectReel("StatusFisher", "uuid-033", true, 0, 0);
Map<String, Object> status = service.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalCastings"), "Should contain totalCastings");
assertTrue(status.containsKey("autoFishViolations"), "Should contain autoFishViolations");
assertTrue(status.containsKey("reactionTimeStats"), "Should contain reactionTimeStats");
assertTrue(status.containsKey("trackedPlayers"), "Should contain trackedPlayers");
}
// ==================== AntiInventoryManipulationService Tests ====================
/**
* 正常背包物品移动应返回 clean。
*/
@Test
void antiInventoryNormalItemMoveReturnsClean() {
AntiInventoryManipulationService service = new AntiInventoryManipulationService();
AntiInventoryManipulationService.DetectionResult r = service.detectItemMove(
"NormalPlayer", "uuid-040", 0, 1, "STONE", 64, -1);
assertTrue(r.isClean(), "Normal item move should be clean");
assertFalse(r.isFlagged());
}
/**
* 访问非法槽位(负索引或超范围)应被检测。
*/
@Test
void antiInventoryInvalidSlotDetected() {
AntiInventoryManipulationService service = new AntiInventoryManipulationService();
// 访问负索引槽位
AntiInventoryManipulationService.DetectionResult r = service.detectItemMove(
"SlotHacker", "uuid-041", -1, 0, "DIAMOND", 1, -1);
assertTrue(r.isFlagged() || r.isSuspicious(),
"Invalid slot access should be detected");
assertTrue(service.getSlotViolations() >= 0);
}
/**
* 批量丢弃(单 tick 丢弃多个物品)应被检测。
* 注意:第 2 次同 tick 丢弃会触发 flagged 标记,后续丢弃被拦截不再计数。
*/
@Test
void antiInventoryBatchDropDetected() {
AntiInventoryManipulationService service = new AntiInventoryManipulationService();
// 快速丢弃多个物品——前两次在同一个 tick,触发批量丢弃检测
AntiInventoryManipulationService.DetectionResult r1 = service.detectItemDrop(
"DropBot", "uuid-042", "COBBLESTONE", 1, 0);
assertTrue(r1.isClean(), "Single drop should be clean");
AntiInventoryManipulationService.DetectionResult r2 = service.detectItemDrop(
"DropBot", "uuid-042", "COBBLESTONE", 1, 1);
// 第二次同 tick 丢弃触发 batch drop 检测
assertTrue(r2.isFlagged() || r2.isSuspicious(),
"Two drops in same tick should be detected as batch drop");
assertTrue(service.getTotalInventoryOps() >= 2,
"At least 2 drops should be recorded before player is flagged");
assertTrue(service.getManipulationViolations() >= 1,
"Batch drop should trigger manipulation violation");
}
/**
* 超范围槽位访问应触发 slotViolations 计数器。
*/
@Test
void antiInventoryOversizedSlotAccessDetected() {
AntiInventoryManipulationService service = new AntiInventoryManipulationService();
// 访问超出背包大小的槽位
AntiInventoryManipulationService.DetectionResult r = service.detectItemMove(
"RangeHacker", "uuid-043", 0, 99, "DIAMOND_BLOCK", 1, -1);
assertTrue(r.isFlagged() || r.isSuspicious(),
"Accessing slot beyond max index should be detected");
assertTrue(service.getSlotViolations() >= 0);
}
/**
* getStatus() 应包含 totalInventoryOps、manipulationViolations、slotViolations。
*/
@Test
void antiInventoryStatusReturnsExpectedFields() {
AntiInventoryManipulationService service = new AntiInventoryManipulationService();
service.detectItemMove("StatusPlayer", "uuid-044", 0, 1, "DIRT", 64, -1);
Map<String, Object> status = service.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalInventoryOps"), "Should contain totalInventoryOps");
assertTrue(status.containsKey("manipulationViolations"), "Should contain manipulationViolations");
assertTrue(status.containsKey("slotViolations"), "Should contain slotViolations");
assertTrue(status.containsKey("trackedPlayers"), "Should contain trackedPlayers");
assertTrue((Long) status.get("totalInventoryOps") >= 1);
}
// ==================== AntiBaritoneService Tests ====================
/**
* 正常玩家移动应返回 clean。
*/
@Test
void antiBaritoneNormalMovementReturnsClean() {
AntiBaritoneService service = new AntiBaritoneService();
// 记录几次正常移动
for (int i = 0; i < 5; i++) {
AntiBaritoneService.DetectionResult r = service.detectMovement(
"NormalPlayer", "uuid-050", 10 + i * 0.3, 64, 10 + i * 0.5,
90.0f, false, false);
// 前几个样本可能因数据不足返回 clean
assertNotNull(r);
}
}
/**
* 无社交交互的长时间在线应被检测为 Bot 行为。
*/
@Test
void antiBaritoneLongSessionNoSocialDetected() {
AntiBaritoneService service = new AntiBaritoneService();
// 模拟 Bot 长时间移动但无社交交互(需要足够样本触发检测)
for (int i = 0; i < 50; i++) {
service.detectMovement(
"SilentBot", "uuid-051",
100 + i * 0.5, 64, 100 + i * 0.5,
(float) (i * 2), i % 2 == 0, false);
}
assertTrue(service.getTotalTracked() >= 1,
"Bot player should be tracked");
}
/**
* 机械式视角移动(恒定角速度)应被检测。
*/
@Test
void antiBaritoneMechanicalLookDetected() {
AntiBaritoneService service = new AntiBaritoneService();
// 模拟恒定角速度的视角移动(Baritone Lerp 特征)
for (int i = 0; i < 35; i++) {
float yaw = (float) (i * 2.0); // 恒定的 2 度增量
float pitch = 0.0f;
AntiBaritoneService.DetectionResult r = service.detectLook(
"LookBot", "uuid-052", yaw, pitch);
assertNotNull(r);
}
}
/**
* 自动聊天快速响应应被检测。
*/
@Test
void antiBaritoneAutoChatFastResponseDetected() {
AntiBaritoneService service = new AntiBaritoneService();
// 模拟 300ms 的极速响应(Baritone AutoChat 特征)
AntiBaritoneService.DetectionResult r = service.detectChat(
"ChatBot", "uuid-053", "Yes, I am mining here", true, 300);
assertTrue(r.isFlagged() || r.isSuspicious(),
"Auto chat response under 500ms should be detected");
}
/**
* 正常聊天消息应返回 clean。
*/
@Test
void antiBaritoneNormalChatReturnsClean() {
AntiBaritoneService service = new AntiBaritoneService();
AntiBaritoneService.DetectionResult r = service.detectChat(
"NormalChatPlayer", "uuid-054",
"Hello everyone!", false, 0);
assertTrue(r.isClean(),
"Normal chat message should be clean");
}
/**
* getStatus() 应包含 totalTracked、baritoneSuspected、confidenceScores。
*/
@Test
void antiBaritoneStatusReturnsExpectedFields() {
AntiBaritoneService service = new AntiBaritoneService();
service.detectMovement("StatusBot", "uuid-055",
0, 64, 0, 0, false, false);
service.detectLook("StatusBot", "uuid-055", 45.0f, 0.0f);
service.detectChat("StatusBot", "uuid-055", "ok", false, 0);
Map<String, Object> status = service.getStatus();
assertNotNull(status);
assertTrue(status.containsKey("totalTracked"), "Should contain totalTracked");
assertTrue(status.containsKey("baritoneSuspected"), "Should contain baritoneSuspected");
assertTrue(status.containsKey("confidenceScores"), "Should contain confidenceScores");
assertTrue(status.containsKey("movementTracked"), "Should contain movementTracked");
assertTrue(status.containsKey("lookTracked"), "Should contain lookTracked");
assertTrue(status.containsKey("interactionTracked"), "Should contain interactionTracked");
}
}
@@ -0,0 +1,404 @@
package com.aluer.security;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* V4.0 服务器保护模块测试类
* 覆盖6个新增安全模块:PacketFloodProtection, AntiSignExploit, AntiBookBan,
* AntiResourcePackExploit, AntiTabCompleteCrash, AntiOfflineModeSpoof
*/
class V40ServerProtectionTest {
// ==================== PacketFloodProtectionService ====================
@Test
void packetFloodAllowsNormalTraffic() {
PacketFloodProtectionService svc = new PacketFloodProtectionService();
// 正常情况下几个数据包不影响
assertTrue(svc.check("Player1", "ChatMessage", null).isClean());
assertTrue(svc.check("Player1", "CustomPayload", "minecraft:register").isClean());
}
@Test
void packetFloodDetectsChatMessageFlood() {
PacketFloodProtectionService svc = new PacketFloodProtectionService();
// 模拟聊天洪水:5秒内发送远超20/s的数据包
for (int i = 0; i < 150; i++) {
svc.check("Flooder1", "ChatMessage", null);
}
PacketFloodProtectionService.PacketFloodResult r = svc.check("Flooder1", "ChatMessage", null);
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("ChatMessage")));
}
@Test
void packetFloodDetectsBrandChannelFlood() {
PacketFloodProtectionService svc = new PacketFloodProtectionService();
// 品牌包在60秒内超过3次即为异常
for (int i = 0; i < 5; i++) {
svc.check("SuspiciousPlayer", "PluginMessage", "minecraft:brand");
}
PacketFloodProtectionService.PacketFloodResult r = svc.check(
"SuspiciousPlayer", "PluginMessage", "minecraft:brand");
assertTrue(r.isBlocked());
}
@Test
void packetFloodIncrementalPenaltyIncreases() {
PacketFloodProtectionService svc = new PacketFloodProtectionService();
// CustomPayload阈值=50/s即250/5s窗口,需发送>250个才触发
for (int i = 0; i < 300; i++) {
svc.check("Attacker1", "CustomPayload", "bad:channel");
}
PacketFloodProtectionService.PacketFloodResult r1 = svc.check("Attacker1", "CustomPayload", "bad:channel");
assertTrue(r1.isBlocked());
// 递增惩罚应该有起步延迟(首次违规100ms)
assertTrue(r1.getPenaltyMs() >= 100);
}
@Test
void packetFloodStatusHasCounters() {
PacketFloodProtectionService svc = new PacketFloodProtectionService();
svc.check("P1", "ChatMessage", null);
svc.check("P2", "CustomPayload", "test:chan");
Map<String, Object> s = svc.getStatus();
assertNotNull(s.get("totalPackets"));
assertNotNull(s.get("blockedCount"));
assertNotNull(s.get("packetTypeStats"));
assertTrue(((Number) s.get("totalPackets")).longValue() >= 2);
}
// ==================== AntiSignExploitService ====================
@Test
void antiSignExploitAllowsNormalSign() {
AntiSignExploitService svc = new AntiSignExploitService();
String[] lines = {"Hello", "World", "", ""};
AntiSignExploitService.SignCheckResult r = svc.check("Player1", "world", 0, 64, 0, lines, 0);
assertTrue(r.isClean());
}
@Test
void antiSignExploitDetectsDeepNBTNesting() {
AntiSignExploitService svc = new AntiSignExploitService();
String[] lines = {"test", "", "", ""};
// NBT深度超过8层
AntiSignExploitService.SignCheckResult r = svc.check("Hacker", "world", 0, 64, 0, lines, 15);
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("NBT_DEEP_NESTING")));
}
@Test
void antiSignExploitDetectsOversizedText() {
AntiSignExploitService svc = new AntiSignExploitService();
// 超过10000字符的单行文本
StringBuilder sb = new StringBuilder();
sb.append("{\"text\":\"");
for (int i = 0; i < 11000; i++) sb.append("A");
sb.append("\"}");
String[] lines = {sb.toString(), "", "", ""};
AntiSignExploitService.SignCheckResult r = svc.check("Hacker", "world", 0, 64, 0, lines, 0);
assertTrue(r.isBlocked());
}
@Test
void antiSignExploitDetectsClickCommandInjection() {
AntiSignExploitService svc = new AntiSignExploitService();
String[] lines = {"{\"text\":\"Click me\",\"clickEvent\":{\"action\":\"run_command\",\"value\":\"/op Hacker\"}}",
"", "", ""};
AntiSignExploitService.SignCheckResult r = svc.check("Hacker", "world", 0, 64, 0, lines, 0);
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("CLICK_COMMAND")));
}
@Test
void antiSignExploitDetectsDangerousPlaceholder() {
AntiSignExploitService svc = new AntiSignExploitService();
String[] lines = {"{\"text\":\"${system:exec:rm -rf /}\"}", "", "", ""};
AntiSignExploitService.SignCheckResult r = svc.check("Hacker", "world", 0, 64, 0, lines, 0);
assertTrue(r.isBlocked());
}
@Test
void antiSignExploitStatusHasCounters() {
AntiSignExploitService svc = new AntiSignExploitService();
svc.check("P1", "world", 0, 64, 0, new String[]{"Hello"}, 0);
Map<String, Object> s = svc.getStatus();
assertNotNull(s.get("totalSignsChecked"));
assertNotNull(s.get("exploitBlocked"));
assertNotNull(s.get("lastBlockedDetails"));
}
// ==================== AntiBookBanService ====================
@Test
void antiBookBanAllowsNormalBook() {
AntiBookBanService svc = new AntiBookBanService();
List<String> pages = List.of("{\"text\":\"Hello World\"}", "{\"text\":\"Page 2\"}");
AntiBookBanService.BookCheckResult r = svc.check("Player1", "My Book", pages, "Player1", 0, 500);
assertTrue(r.isClean());
}
@Test
void antiBookBanDetectsExcessivePages() {
AntiBookBanService svc = new AntiBookBanService();
// 超过100页
List<String> pages = new ArrayList<>();
for (int i = 0; i < 150; i++) {
pages.add("{\"text\":\"Page " + i + "\"}");
}
AntiBookBanService.BookCheckResult r = svc.check("Hacker", "Bad Book", pages, "Hacker", 0, 5000);
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("EXCESSIVE_PAGES")));
}
@Test
void antiBookBanDetectsOversizedPage() {
AntiBookBanService svc = new AntiBookBanService();
// 单页超过32767字符
StringBuilder sb = new StringBuilder("{\"text\":\"");
for (int i = 0; i < 33000; i++) sb.append("B");
sb.append("\"}");
List<String> pages = List.of(sb.toString());
AntiBookBanService.BookCheckResult r = svc.check("Hacker", "Overflow Book", pages, "Hacker", 0, 50000);
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("OVERSIZED")));
}
@Test
void antiBookBanDetectsDangerousTranslate() {
AntiBookBanService svc = new AntiBookBanService();
// translate组件引用危险翻译键
List<String> pages = List.of("{\"translate\":\"translation.test.none\",\"color\":\"red\"}");
AntiBookBanService.BookCheckResult r = svc.check("Hacker", "Translate Exploit", pages, "Hacker", 0, 200);
assertTrue(r.isFlagged() || r.isBlocked());
}
@Test
void antiBookBanDetectsDeepJsonNesting() {
AntiBookBanService svc = new AntiBookBanService();
// 构造深度超过15层的嵌套JSON
StringBuilder sb = new StringBuilder("{\"text\":\"x\"");
for (int i = 0; i < 20; i++) {
sb = new StringBuilder("{\"extra\":[" + sb + "]}");
}
List<String> pages = List.of(sb.toString());
AntiBookBanService.BookCheckResult r = svc.check("Hacker", "Deep Nest Book", pages, "Hacker", 0, 500);
assertTrue(r.isFlagged() || r.isBlocked());
}
@Test
void antiBookBanStatusHasCounters() {
AntiBookBanService svc = new AntiBookBanService();
svc.check("P1", "Test", List.of("{\"text\":\"Hello\"}"), "P1", 0, 100);
Map<String, Object> s = svc.getStatus();
assertNotNull(s.get("totalBooksChecked"));
assertNotNull(s.get("bannedBooks"));
}
// ==================== AntiResourcePackExploitService ====================
@Test
void antiResourcePackAllowsNormalPack() {
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
// SHA-1必须是40位十六进制字符,否则格式校验会拒绝
AntiResourcePackExploitService.PackCheckResult r = svc.check(
"https://example.com/resourcepack.zip",
"a1b2c3d4e5f6789012345678901234567890abcd",
"Admin", 9);
assertTrue(r.isClean());
}
@Test
void antiResourcePackDetectsFileScheme() {
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
AntiResourcePackExploitService.PackCheckResult r = svc.check(
"file:///etc/passwd",
"a1b2c3d4e5f6789012345678901234567890abc",
"Hacker", 9);
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("SCHEME") || s.contains("BLOCKED")));
}
@Test
void antiResourcePackDetectsInternalNetwork() {
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
AntiResourcePackExploitService.PackCheckResult r = svc.check(
"http://192.168.1.100/malware.zip",
"a1b2c3d4e5f6789012345678901234567890abc",
"Hacker", 9);
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("PRIVATE") || s.contains("SSRF")));
}
@Test
void antiResourcePackDetectsBlacklistedSha1() {
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
String maliciousSha1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
svc.addToSha1Blacklist(maliciousSha1);
AntiResourcePackExploitService.PackCheckResult r = svc.check(
"https://cdn.example.com/pack.zip", maliciousSha1, "Admin", 9);
assertTrue(r.isBlocked());
}
@Test
void antiResourcePackDetectsDangerousExtension() {
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
AntiResourcePackExploitService.PackCheckResult r = svc.check(
"https://evil.com/trojan.jar",
"a1b2c3d4e5f6789012345678901234567890abc",
"Hacker", 9);
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("DANGEROUS") || s.contains("EXTENSION")));
}
@Test
void antiResourcePackStatusHasCounters() {
AntiResourcePackExploitService svc = new AntiResourcePackExploitService();
svc.check("https://safe.com/pack.zip", "a1b2c3d4e5f6789012345678901234567890abc", "Admin", 9);
Map<String, Object> s = svc.getStatus();
assertNotNull(s.get("totalPacksChecked"));
assertNotNull(s.get("blockedPacks"));
assertNotNull(s.get("urlBlacklistSize"));
}
// ==================== AntiTabCompleteCrashService ====================
@Test
void antiTabCompleteAllowsNormalRequest() {
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
AntiTabCompleteCrashService.TabCheckResult r = svc.check("Player1", "gamemode c", 8, 1);
assertTrue(r.isClean());
}
@Test
void antiTabCompleteDetectsOversizedRequest() {
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
// 构造超过8192字符的请求
StringBuilder sb = new StringBuilder("execute as @e[tag=");
for (int i = 0; i < 10000; i++) sb.append("a");
AntiTabCompleteCrashService.TabCheckResult r = svc.check("Hacker", sb.toString(), 0, 1);
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("OVERSIZED")));
}
@Test
void antiTabCompleteDetectsDeepNesting() {
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
// 构造深度超过50层的嵌套括号
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 60; i++) sb.append("(\"");
sb.append("test");
for (int i = 0; i < 60; i++) sb.append("\")");
AntiTabCompleteCrashService.TabCheckResult r = svc.check("Hacker", sb.toString(), 0, 1);
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("NESTING")));
}
@Test
void antiTabCompleteDetectsTabFlood() {
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
// 高频补全请求洪水
for (int i = 0; i < 150; i++) {
svc.check("Flooder", "command " + i, 8, i);
}
AntiTabCompleteCrashService.TabCheckResult r = svc.check("Flooder", "command x", 8, 999);
assertTrue(r.isBlocked() || r.isSuspicious());
}
@Test
void antiTabCompleteDetectsInvalidUtf8() {
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
// 包含无效UTF-8字节序列的请求
String badText = "testbadchars";
// 注意:Java不会阻止,但我们应该检测它
AntiTabCompleteCrashService.TabCheckResult r = svc.check("Hacker", badText, 0, 1);
// null字符应被检测到
assertTrue(r.isBlocked() || r.isSuspicious());
}
@Test
void antiTabCompleteStatusHasCounters() {
AntiTabCompleteCrashService svc = new AntiTabCompleteCrashService();
svc.check("P1", "help", 4, 1);
Map<String, Object> s = svc.getStatus();
assertNotNull(s.get("totalTabRequests"));
assertNotNull(s.get("crashPrevented"));
}
// ==================== AntiOfflineModeSpoofService ====================
@Test
void antiOfflineModeAllowsValidLogin() {
AntiOfflineModeSpoofService svc = new AntiOfflineModeSpoofService();
AntiOfflineModeSpoofService.SpoofCheckResult r = svc.check(
"550e8400-e29b-41d4-a716-446655440000",
"Player1", "192.168.1.1", "session-001");
assertTrue(r.isClean());
}
@Test
void antiOfflineModeDetectsInvalidUUIDFormat() {
AntiOfflineModeSpoofService svc = new AntiOfflineModeSpoofService();
// 完全无效的UUID
AntiOfflineModeSpoofService.SpoofCheckResult r = svc.check(
"not-a-valid-uuid-at-all",
"Hacker", "10.0.0.1", "session-001");
assertTrue(r.isBlocked());
}
@Test
void antiOfflineModeDetectsRapidUuidSwitch() {
AntiOfflineModeSpoofService svc = new AntiOfflineModeSpoofService();
// 同一IP在短时间内使用多个不同UUID
svc.check("550e8400-e29b-41d4-a716-446655440001", "Alt1", "10.0.0.50", "s1");
svc.check("550e8400-e29b-41d4-a716-446655440002", "Alt2", "10.0.0.50", "s2");
svc.check("550e8400-e29b-41d4-a716-446655440003", "Alt3", "10.0.0.50", "s3");
AntiOfflineModeSpoofService.SpoofCheckResult r = svc.check(
"550e8400-e29b-41d4-a716-446655440004", "Alt4", "10.0.0.50", "s4");
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("RAPID_UUID_SWITCH")));
}
@Test
void antiOfflineModeDetectsAdminNameSpoof() {
AntiOfflineModeSpoofService svc = new AntiOfflineModeSpoofService();
// 用户名是admin但UUID格式异常
AntiOfflineModeSpoofService.SpoofCheckResult r = svc.check(
"invalid-uuid-x",
"Admin", "10.0.0.1", "session-001");
assertTrue(r.isBlocked());
}
@Test
void antiOfflineModeDetectsProtectedUuid() {
AntiOfflineModeSpoofService svc = new AntiOfflineModeSpoofService();
String protectedUuid = "aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa";
svc.addProtectedUuid(protectedUuid);
AntiOfflineModeSpoofService.SpoofCheckResult r = svc.check(
protectedUuid, "Imposter", "192.168.1.1", "session-001");
assertTrue(r.isBlocked());
assertTrue(r.getReasons().stream().anyMatch(s -> s.contains("PROTECTED")));
}
@Test
void antiOfflineModeNormalPlayerNotAffected() {
AntiOfflineModeSpoofService svc = new AntiOfflineModeSpoofService();
// 多个不同IP的正常UUID加入不应被阻止
assertTrue(svc.check("550e8400-e29b-41d4-a716-446655440010", "Alice", "1.1.1.1", "s1").isClean());
assertTrue(svc.check("550e8400-e29b-41d4-a716-446655440020", "Bob", "2.2.2.2", "s2").isClean());
assertTrue(svc.check("550e8400-e29b-41d4-a716-446655440030", "Carol", "3.3.3.3", "s3").isClean());
}
@Test
void antiOfflineModeStatusHasCounters() {
AntiOfflineModeSpoofService svc = new AntiOfflineModeSpoofService();
svc.check("550e8400-e29b-41d4-a716-446655440100", "TestPlayer", "1.1.1.1", "ss");
Map<String, Object> s = svc.getStatus();
assertNotNull(s.get("totalJoins"));
assertNotNull(s.get("spoofBlocked"));
assertNotNull(s.get("ipUuidMap"));
}
}