Files
Aluer/src/test/java/com/aluer/security/V40ChatSecurityTest.java
T
Peijun Zhao 1269d4dfa5 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: 重构检测流程,移除提前返回导致的检测屏蔽
2026-05-16 11:47:59 +08:00

362 lines
15 KiB
Java

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"));
}
}