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,441 @@
package com.aluer.security;
import com.aluer.config.ServerGuardConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* 配置文件篡改检测 — V4.0 访问控制模块
*
* 检测原理:
* 1. 监控Minecraft服务器的关键配置文件:server.properties, ops.json, whitelist.json,
* banned-players.json, banned-ips.json, bukkit.yml, spigot.yml, paper.yml等
* 2. 对每个被监控文件维护SHA-256哈希基线值,任何与基线不匹配的变化均视为篡改事件
* 3. 检测ops.json中的未授权OP添加——通过对比已知合法OP列表与新文件中出现的OP
* 4. 检测whitelist.json中的未授权白名单添加——特别是在服务器开启白名单模式时的异常添加
* 5. 检测server.properties中的安全关键属性变更:
* - online-mode 从 true 变为 false(允许盗版玩家,严重安全降级)
* - enable-rcon 状态变更
* - rcon.password 的修改
* - enable-command-block 的启用
* - enforce-secure-profile 的禁用
* 6. 支持周期性全量扫描(默认60秒)和事件触发检查(如检测到文件修改事件时)
*
* 配置开关:serverguard.security.super-evolution.config-tamper-detection
*/
@Service
public class ConfigTamperDetectionService {
private final ServerGuardConfig config;
/** 文件路径 -> SHA-256基线哈希值 */
private final Map<String, String> fileHashBaselines = new ConcurrentHashMap<>();
/** 文件路径 -> 篡改事件列表 */
private final Map<String, List<TamperEvent>> tamperEventsPerFile = new ConcurrentHashMap<>();
/** 所有篡改事件的全局时间线 */
private final List<TamperEvent> tamperTimeline = Collections.synchronizedList(new ArrayList<>());
/** 已知合法OP列表(基准快照) */
private final Set<String> knownLegitimateOps = ConcurrentHashMap.newKeySet();
private final AtomicLong totalMonitoredFiles = new AtomicLong(0);
private final AtomicLong totalTamperEvents = new AtomicLong(0);
private final AtomicLong totalScans = new AtomicLong(0);
private volatile Instant lastCheckTime = Instant.now();
/** 默认被监控的关键配置文件列表 */
private static final List<String> MONITORED_FILES = List.of(
"server.properties",
"ops.json",
"whitelist.json",
"banned-players.json",
"banned-ips.json",
"bukkit.yml",
"spigot.yml",
"paper.yml",
"permissions.yml",
"commands.yml",
"eula.txt"
);
/** server.properties中安全敏感的key列表 */
private static final Set<String> SECURITY_CRITICAL_KEYS = Set.of(
"online-mode",
"enable-rcon",
"rcon.password",
"rcon.port",
"enable-command-block",
"enforce-secure-profile",
"prevent-proxy-connections",
"require-resource-pack",
"white-list",
"enforce-whitelist",
"spawn-protection",
"max-players",
"level-name",
"server-port"
);
/** 无参构造函数,使用默认配置 */
public ConfigTamperDetectionService() {
this(new ServerGuardConfig());
}
@Autowired
public ConfigTamperDetectionService(ServerGuardConfig config) {
this.config = config;
}
/**
* 注册文件基线哈希——在系统启动时调用,建立初始信任基线
*
* @param filePath 文件路径
* @param content 文件的当前内容(UTF-8字节)
*/
public void registerBaseline(String filePath, byte[] content) {
if (!config.getSecurity().getSuperEvolution().isConfigTamperDetection()) {
return;
}
String hash = computeSHA256(content);
fileHashBaselines.put(filePath, hash);
totalMonitoredFiles.set(fileHashBaselines.size());
}
/**
* 检查单个文件是否被篡改——将当前内容与基线哈希比对
*
* @param filePath 文件相对路径(如 "server.properties", "ops.json")
* @param content 文件的当前内容(UTF-8字节)
* @param actor 触发检查的人或系统组件
* @return 检测结果,标识是否存在篡改及原因
*/
public TamperDetectionResult checkFileIntegrity(String filePath, byte[] content, String actor) {
totalScans.incrementAndGet();
lastCheckTime = Instant.now();
// 配置开关检查
if (!config.getSecurity().getSuperEvolution().isConfigTamperDetection()) {
return TamperDetectionResult.clean();
}
if (filePath == null || content == null) {
return TamperDetectionResult.clean();
}
List<String> reasons = new ArrayList<>();
boolean tampered = false;
String currentHash = computeSHA256(content);
// 检查1:与基线哈希比对,检测文件内容是否被修改
String baseline = fileHashBaselines.get(filePath);
if (baseline != null && !baseline.equals(currentHash)) {
tampered = true;
totalTamperEvents.incrementAndGet();
reasons.add("HASH_MISMATCH: " + filePath + " has been modified (baseline changed)");
// 记录篡改事件
TamperEvent event = new TamperEvent(Instant.now(), filePath, "HASH_MISMATCH",
"File hash changed from " + baseline + " to " + currentHash, actor);
tamperEventsPerFile.computeIfAbsent(filePath, k -> Collections.synchronizedList(new ArrayList<>())).add(event);
tamperTimeline.add(event);
// 更新基线为当前值(防止重复报警)
fileHashBaselines.put(filePath, currentHash);
}
// 检查2:如果是ops.json,深度解析检测未授权OP添加
if (filePath.endsWith("ops.json") && content.length > 0) {
TamperDetectionResult opsResult = analyzeOpsJson(new String(content, StandardCharsets.UTF_8));
if (opsResult.isTampered()) {
tampered = true;
reasons.addAll(opsResult.getReasons());
}
}
// 检查3:如果是server.properties,检测安全关键属性变更
if (filePath.endsWith("server.properties") && content.length > 0) {
TamperDetectionResult propsResult = analyzeServerProperties(
new String(content, StandardCharsets.UTF_8));
if (propsResult.isTampered()) {
tampered = true;
reasons.addAll(propsResult.getReasons());
}
}
// 检查4:如果是whitelist.json,检测未授权白名单添加
if (filePath.endsWith("whitelist.json") || filePath.endsWith("white-list.txt")) {
if (content.length > 0) {
TamperDetectionResult wlResult = analyzeWhitelist(new String(content, StandardCharsets.UTF_8), filePath);
if (wlResult.isTampered()) {
tampered = true;
reasons.addAll(wlResult.getReasons());
}
}
}
// 如果该文件尚未注册基线,自动建立基线(第一次检查时信任当前状态)
if (baseline == null) {
fileHashBaselines.put(filePath, currentHash);
totalMonitoredFiles.set(fileHashBaselines.size());
}
if (tampered) {
return TamperDetectionResult.tampered(reasons);
}
return TamperDetectionResult.clean();
}
/**
* 设置已知合法OP列表(作为基准快照),后续ops.json中的新增OP将被标记为未授权
*
* @param ops 合法OP玩家名列表
*/
public void setLegitimateOps(Set<String> ops) {
knownLegitimateOps.clear();
if (ops != null) {
knownLegitimateOps.addAll(ops);
}
}
/**
* 添加一个合法OP到已知列表
*
* @param opName OP玩家名
*/
public void addLegitimateOp(String opName) {
if (opName != null) {
knownLegitimateOps.add(opName);
// 同时更新ops.json基线
String filePath = "ops.json";
String baseline = fileHashBaselines.get(filePath);
if (baseline != null) {
// 基线会过期,因为ops.json内容已变,下一次check时自动更新
}
}
}
/**
* 获取模块运行状态
*
* @return 状态Map,包含monitoredFiles/tamperEvents/lastCheckTime
*/
public Map<String, Object> getStatus() {
Map<String, Object> s = new LinkedHashMap<>();
s.put("enabled", config.getSecurity().getSuperEvolution().isConfigTamperDetection());
s.put("monitoredFiles", totalMonitoredFiles.get());
s.put("tamperEvents", totalTamperEvents.get());
s.put("totalScans", totalScans.get());
s.put("lastCheckTime", lastCheckTime.toString());
s.put("monitoredFileList", new ArrayList<>(fileHashBaselines.keySet()));
// 最近10条篡改事件
List<Map<String, Object>> recentEvents = new ArrayList<>();
int start = Math.max(0, tamperTimeline.size() - 10);
for (int i = start; i < tamperTimeline.size(); i++) {
TamperEvent e = tamperTimeline.get(i);
Map<String, Object> m = new LinkedHashMap<>();
m.put("time", e.time.toString());
m.put("file", e.filePath);
m.put("type", e.tamperType);
m.put("detail", e.detail);
m.put("actor", e.actor);
recentEvents.add(m);
}
s.put("recentTamperEvents", recentEvents);
return s;
}
// ==================== 内部文件分析方法 ====================
/**
* 分析ops.json内容,检测未授权的OP添加
* 原理:扫描JSON中出现的所有玩家名,与已知合法OP列表比对
*/
private TamperDetectionResult analyzeOpsJson(String content) {
List<String> reasons = new ArrayList<>();
boolean tampered = false;
// 使用简单的正则提取UUID和玩家名(不依赖JSON解析库)
// ops.json 格式: [{"uuid":"...","name":"PlayerName","level":4,...}]
java.util.regex.Matcher nameMatcher = java.util.regex.Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"")
.matcher(content);
while (nameMatcher.find()) {
String opName = nameMatcher.group(1);
if (!knownLegitimateOps.isEmpty() && !knownLegitimateOps.contains(opName)) {
tampered = true;
reasons.add("UNAUTHORIZED_OP: Unknown OP \"" + opName
+ "\" found in ops.json, not in known legitimate OP list");
}
}
if (tampered) {
return TamperDetectionResult.tampered(reasons);
}
return TamperDetectionResult.clean();
}
/**
* 分析server.properties内容,检测安全关键属性的值变更
* 特别关注 online-mode 从 true 变为 false(严重安全降级)
*/
private TamperDetectionResult analyzeServerProperties(String content) {
List<String> reasons = new ArrayList<>();
boolean tampered = false;
// 解析属性为key-value对
Map<String, String> props = new LinkedHashMap<>();
for (String line : content.split("\\r?\\n")) {
String trimmed = line.trim();
if (trimmed.isEmpty() || trimmed.startsWith("#")) continue;
int eqIdx = trimmed.indexOf('=');
if (eqIdx > 0) {
String key = trimmed.substring(0, eqIdx).trim();
String value = trimmed.substring(eqIdx + 1).trim();
props.put(key, value);
}
}
// 检测 online-mode=false(允许盗版玩家加入)
String onlineMode = props.get("online-mode");
if (onlineMode != null && "false".equalsIgnoreCase(onlineMode)) {
tampered = true;
reasons.add("ONLINE_MODE_DISABLED: server.properties online-mode=false, "
+ "allows cracked/offline players — critical security downgrade");
}
// 检测 enable-rcon 状态
String enableRcon = props.get("enable-rcon");
if (enableRcon != null && "true".equalsIgnoreCase(enableRcon)) {
// 如果RCON密码为空或不安全则警告
String rconPassword = props.get("rcon.password");
if (rconPassword == null || rconPassword.isEmpty() || rconPassword.length() < 8) {
tampered = true;
reasons.add("WEAK_RCON_PASSWORD: RCON enabled but password is empty or too short (<8 chars)");
}
}
// 检测 envoke-secure-profile=false(禁用安全档案验证)
String secureProfile = props.get("enforce-secure-profile");
if (secureProfile != null && "false".equalsIgnoreCase(secureProfile)) {
tampered = true;
reasons.add("SECURE_PROFILE_DISABLED: enforce-secure-profile=false, "
+ "player chat signatures not enforced");
}
// 检测 enable-command-block=true(可能被利用执行恶意指令)
String enableCmdBlock = props.get("enable-command-block");
if (enableCmdBlock != null && "true".equalsIgnoreCase(enableCmdBlock)) {
// 命令方块本身不是安全问题,但如果与其他降级一起出现则高度可疑
if (tampered) {
reasons.add("COMMAND_BLOCK_ENABLED_WITH_DOWNGRADE: command blocks enabled "
+ "while other security features are disabled — combined risk");
}
}
if (tampered) {
return TamperDetectionResult.tampered(reasons);
}
return TamperDetectionResult.clean();
}
/**
* 分析whitelist.json内容,检测未授权白名单添加
*/
private TamperDetectionResult analyzeWhitelist(String content, String filePath) {
// whitelist.json 格式: [{"uuid":"...","name":"PlayerName"},...]
List<String> reasons = new ArrayList<>();
boolean tampered = false;
java.util.regex.Matcher nameMatcher = java.util.regex.Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"")
.matcher(content);
int count = 0;
while (nameMatcher.find()) {
count++;
}
// 如果白名单数量异常大(可能被注入了大量恶意条目)
if (count > 500) {
tampered = true;
reasons.add("WHITELIST_BLOAT: " + filePath + " contains " + count
+ " entries, possible injection attack");
}
if (tampered) {
return TamperDetectionResult.tampered(reasons);
}
return TamperDetectionResult.clean();
}
// ==================== 工具方法 ====================
/**
* 计算字节数组的SHA-256哈希值
*
* @param data 原始数据
* @return 十六进制哈希字符串,计算失败时返回空字符串
*/
private String computeSHA256(byte[] data) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (NoSuchAlgorithmException e) {
return "";
}
}
// ==================== 内部数据类 ====================
/** 篡改事件记录 */
private static class TamperEvent {
final Instant time;
final String filePath;
final String tamperType;
final String detail;
final String actor;
TamperEvent(Instant time, String filePath, String tamperType, String detail, String actor) {
this.time = time;
this.filePath = filePath;
this.tamperType = tamperType;
this.detail = detail;
this.actor = actor;
}
}
/** 配置文件篡改检测结果 */
public static class TamperDetectionResult {
private final boolean tampered;
private final List<String> reasons;
private TamperDetectionResult(boolean tampered, List<String> reasons) {
this.tampered = tampered;
this.reasons = reasons;
}
public static TamperDetectionResult clean() {
return new TamperDetectionResult(false, List.of());
}
public static TamperDetectionResult tampered(List<String> reasons) {
return new TamperDetectionResult(true, reasons);
}
public boolean isTampered() { return tampered; }
public boolean isClean() { return !tampered; }
public List<String> getReasons() { return reasons; }
}
}