V5.0 安全模块大扩展:Meteor Client 全功能反作弊 + AI行为分析 + 服务器保护

新增 45 个安全模块,覆盖 Meteor Client 全部 hack 类别:

【反作弊基础扩展 6 模块】
- AntiTimerService: 游戏加速检测(移动tick间隔分析)
- AntiVelocityService: 击退修改检测(伤害后速度分析)
- AntiPhaseService: 穿墙检测(固体方块射线追踪)
- AntiBlinkService: 闪烁检测(伤害-断连-重连时序)
- AntiFastBreakService: 快速破坏检测(方块标准破坏时间对比)
- AntiElytraFlyService: 鞘翅飞行检测(速度/高度操控分析)

【Meteor Combat 10 模块】
- AntiCriticalsService: 自动暴击检测(跳跃/地面状态验证)
- AntiAutoCrystalService: 水晶Aura检测(放置/爆炸时序分析)
- AntiAutoTotemService: 自动图腾检测(副手装备速度分析)
- AntiSurroundService: 自动包围检测(脚下方块精确放置模式)
- AntiAutoTrapService: 自动陷阱检测(目标周围方块网格分析)
- AntiAutoArmorService: 自动装备检测(多槽位同tick交换)
- AntiChestSwapService: 自动胸甲交换检测(鞘翅-胸甲乒乓切换)
- AntiAutoLogService: 自动断线检测(伤害-断连500ms时序)
- AntiHitboxesService: 扩展碰撞箱检测(射线-边界边缘命中率)
- AntiBowAimBotService: 弓箭自瞄检测(移动目标精度分析)

【Meteor Movement 10 模块】
- AntiNoSlowService: 无减速检测(物品使用+移动速度组合)
- AntiSpiderService: 爬墙检测(无攀爬方块垂直上升)
- AntiStepService: 自动跨步检测(非跳跃1格上升)
- AntiPacketFlyService: 数据包飞行检测(连续悬浮/弹跳模式)
- AntiAirJumpService: 空中跳跃检测(离地跳跃包)
- AntiLongJumpService: 远跳检测(跳跃后水平速度衰减分析)
- AntiAntiHungerService: 免饥饿检测(活动量vs饥饿消耗率)
- AntiFastFallService: 快速下落检测(超终端速度持续下落)
- AntiVClipService: 垂直穿墙检测(固体方块间瞬移)
- AntiNoFallService 增强: 新增GroundSpoof/数据包地面/坠落累积/瞬移坠落检测

【Meteor World+Player 10 模块】
- AntiSpeedMineService: 加速挖掘检测(方块标准破坏时间对比)
- AntiFastUseService: 快速使用检测(物品冷却时间分析)
- AntiNoInteractService: 交互绕过检测(持剑开箱等异常操作)
- AntiAutoMineService: 自动挖掘检测(挖掘间隔方差/视角追踪)
- AntiVeinMinerService: 连锁挖矿检测(相邻矿石快速序列)
- AntiAutoToolService: 自动工具检测(同tick切工具+开挖)
- AntiFakePlayerService: 假人检测(心跳响应/交互缺失/移动模式)
- AntiPistonAuraService: 活塞光环检测(战斗上下文活塞激活率)
- AntiAnchorService: 锚定检测(受击零位移+封闭空间)
- AntiStashFinderService: 仓库扫描检测(网格区块加载+零交互)

【网络协议安全 5 模块】
- ProtocolStateValidator: 协议状态机验证(四态转换违规检测)
- TokenBucketRateLimiter: 令牌桶速率限制器(可复用速率控制基元)
- BotFingerprintDetector: 机器人指纹检测(五维行为指纹分析)
- NBTExploitPrevention: NBT漏洞防护(深度/大小/书页/告示牌验证)
- ConnectionHandshakeValidator: 握手完整性验证(协议版本/状态/主机名)

【服务器保护 4 模块】
- ChunkLoadRateLimiter: 区块加载限制器(WARN/LIMIT/BLOCK三级响应)
- EntityCountEnforcer: 实体数量执行器(分块限制+优先级清理)
- RedstoneUpdateLimiter: 红石更新限制器(三级渐进+观察者钟检测)
- CrashExploitSignatureDB: 崩溃漏洞签名库(12种已知漏洞+4级自动响应)

【AI/ML 行为分析 4 模块】
- BehavioralProfilingEngine: 行为分析引擎(5维特征+香农熵+Z分数)
- ThreatScoreAggregator: 威胁分数聚合器(指数衰减+4级上报)
- MovementPatternAnalyzer: 移动模式分析器(FFT频谱+旋转分析)
- CombatPatternRecognizer: 战斗模式识别器(CPS方差+多目标熵+扫视分析)

配置更新:AlertType新增39个枚举值,SuperEvolutionConfig新增39个开关,
application.yml新增39个配置项。323项全量测试通过,0失败。
This commit is contained in:
Peijun Zhao
2026-05-16 18:02:49 +08:00
parent 67e873525b
commit 1b66197804
54 changed files with 23497 additions and 1 deletions
@@ -0,0 +1,458 @@
package com.aluer.security;
import com.aluer.config.ServerGuardConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* 区块加载速率限制器 — V5.0 服务器保护模块
*
* 检测原理:
* Minecraft 区块加载是服务器最消耗 CPU 和内存的操作之一。正常玩家移动
* 时每秒加载约 1-5 个区块(视距 8-12)。Xray 透视、自动化探索 Bot 或
* 恶意玩家的区块轰炸可在单秒内加载 50+ 个区块,导致服务器严重卡顿甚至崩溃。
*
* 三类危险场景:
* 1. Xray / 矿物透视 — 客户端通过快速遍历区块定位钻石/远古残骸等稀有矿物,
* 需要极快速的区块加载来暴露地下结构。
* 2. Chunk Ban 攻击 — 攻击者故意飞行到世界边缘(X/Z 超过 3000 万),
* 利用极端坐标的区块生成计算量使服务器 OOM 或永久崩溃。
* 3. 自动化探索 Bot — Baritone 等 AI 寻路程序在极短时间内覆盖大片区域,
* 区块加载频率远超正常人类玩家。
*
* 三级梯次响应:
* WARN (>20区块/秒) — 记录警告日志,继续观察
* LIMIT (>40区块/秒) — 跳过部分区块加载请求,对玩家发送速率受限通知
* BLOCK (>60区块/秒) — 完全阻止该玩家的区块加载,踢出玩家
*
* 特殊豁免:
* - 出生点区块加载(登录瞬间的初始加载)
* - 服务器内部区块加载(非玩家触发的世界生成/实体 AI)
* - 管理员白名单玩家
*
* 配置开关:serverguard.security.super-evolution.chunk-load-rate-limiter
*/
@Service
public class ChunkLoadRateLimiter {
private final ServerGuardConfig config;
/** 每个玩家的滑动窗口区块加载记录(保留最近 60 秒) */
private final Map<String, List<ChunkLoadEvent>> playerLoadHistory = new ConcurrentHashMap<>();
/** 每个玩家当前所处的阶段(WARN / LIMIT / BLOCK) */
private final Map<String, Stage> playerStage = new ConcurrentHashMap<>();
/** 每个玩家的限流冷却截止时间 */
private final Map<String, Instant> playerCooldownUntil = new ConcurrentHashMap<>();
/** 每个世界的总区块加载计数(用于服务器健康监控) */
private final Map<String, AtomicLong> worldChunkLoadTotal = new ConcurrentHashMap<>();
/** 极端坐标区块加载事件(X或Z超过30M),用于 chunk ban 检测 */
private final Map<String, List<ExtremeCoordEvent>> extremeCoordEvents = new ConcurrentHashMap<>();
/** 总区块加载请求计数 */
private final AtomicLong totalChunkLoads = new AtomicLong(0);
/** 被阻止的区块加载计数 */
private final AtomicLong blockedChunkLoads = new AtomicLong(0);
/** chunk ban 攻击检测次数 */
private final AtomicLong chunkBanDetections = new AtomicLong(0);
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
/** 区块加载速率阈值(区块/秒) */
private static final int WARN_THRESHOLD = 20;
private static final int LIMIT_THRESHOLD = 40;
private static final int BLOCK_THRESHOLD = 60;
/** 滑窗大小(秒) */
private static final long WINDOW_SECONDS = 5;
/** 限流冷却时间(秒) */
private static final long COOLDOWN_WARN_SECONDS = 10;
private static final long COOLDOWN_LIMIT_SECONDS = 30;
private static final long COOLDOWN_BLOCK_SECONDS = 120;
/** 极端坐标阈值(距原点超30M格的区块) */
private static final int EXTREME_COORD_THRESHOLD = 30_000_000;
/** 记录保留时间(秒) */
private static final long RECORD_RETENTION_SECONDS = 300;
/** 区块加载响应阶段 */
public enum Stage {
NORMAL, // 正常
WARN, // 警告
LIMIT, // 限流
BLOCK // 阻断
}
public ChunkLoadRateLimiter() {
this(new ServerGuardConfig());
}
@Autowired
public ChunkLoadRateLimiter(ServerGuardConfig config) {
this.config = config;
// 每 60 秒清理一次过期数据,防止内存泄漏
scheduler.scheduleAtFixedRate(this::cleanupOldRecords, 60, 60, TimeUnit.SECONDS);
}
/**
* 检查一次区块加载请求是否应该被允许。
*
* 调用时机:Paper 的 ChunkLoadEvent 触发时,或代理拦截到区块数据包时。
*
* @param playerName 触发加载的玩家名称(可为 null 表示服务器内部加载)
* @param playerUUID 玩家 UUID
* @param worldName 世界名称
* @param chunkX 区块 X 坐标
* @param chunkZ 区块 Z 坐标
* @param isServerInternal 是否为服务器内部触发的加载(非玩家移动所致)
* @return 限流检查结果
*/
public ChunkLimitResult checkChunkLoad(String playerName, String playerUUID,
String worldName, int chunkX, int chunkZ,
boolean isServerInternal) {
// 配置开关检查
if (!config.getSecurity().getSuperEvolution().isChunkLoadRateLimiter()) {
return ChunkLimitResult.clean();
}
totalChunkLoads.incrementAndGet();
// 更新世界级别统计(所有加载都计数,包括服务器内部)
worldChunkLoadTotal.computeIfAbsent(worldName, k -> new AtomicLong(0)).incrementAndGet();
// 服务器内部加载直接放行(世界生成、实体AI导致的被动加载)
if (isServerInternal || playerName == null) {
return ChunkLimitResult.clean();
}
// 检查玩家是否处于冷却期
Instant cooldownUntil = playerCooldownUntil.get(playerName);
if (cooldownUntil != null) {
if (Instant.now().isBefore(cooldownUntil)) {
// 仍在冷却期,直接返回当前阶段的阻止状态
Stage currentStage = playerStage.getOrDefault(playerName, Stage.NORMAL);
if (currentStage == Stage.BLOCK) {
blockedChunkLoads.incrementAndGet();
return ChunkLimitResult.blocked(List.of(
"COOLDOWN_BLOCK: player " + playerName + " is blocked until " + cooldownUntil));
} else if (currentStage == Stage.LIMIT) {
// 限流阶段:随机拒绝 50% 的请求
if (Math.random() < 0.5) {
blockedChunkLoads.incrementAndGet();
return ChunkLimitResult.blocked(List.of(
"COOLDOWN_LIMIT: throttled load for " + playerName));
}
}
} else {
// 冷却已过期,清除记录
playerCooldownUntil.remove(playerName);
playerStage.remove(playerName);
}
}
// 记录本次区块加载
Instant now = Instant.now();
ChunkLoadEvent event = new ChunkLoadEvent(now, chunkX, chunkZ, worldName);
List<ChunkLoadEvent> events = playerLoadHistory.computeIfAbsent(playerName,
k -> Collections.synchronizedList(new ArrayList<>()));
events.add(event);
// === 检测1:极端坐标 Chunk Ban 攻击检测 ===
// 区块坐标超过 30M 格(1875000 个区块)为极端坐标
// 攻击者通过 TP 或飞行到达这些位置使服务器尝试生成不可能的地形
if (Math.abs(chunkX) > EXTREME_COORD_THRESHOLD || Math.abs(chunkZ) > EXTREME_COORD_THRESHOLD) {
ExtremeCoordEvent extremeEvent = new ExtremeCoordEvent(now, playerName, chunkX, chunkZ, worldName);
extremeCoordEvents.computeIfAbsent(playerName, k -> new ArrayList<>()).add(extremeEvent);
chunkBanDetections.incrementAndGet();
// 极端坐标无需等待速率累积,直接 BLOCK
playerStage.put(playerName, Stage.BLOCK);
playerCooldownUntil.put(playerName, Instant.now().plusSeconds(COOLDOWN_BLOCK_SECONDS));
blockedChunkLoads.incrementAndGet();
return ChunkLimitResult.flagged(List.of(
"CHUNK_BAN_ATTACK: extreme coordinate chunk load at (" + chunkX + ", " + chunkZ
+ "), distance from origin exceeds " + EXTREME_COORD_THRESHOLD + " blocks"));
}
// === 检测2:区块加载速率检测(5 秒滑动窗口) ===
long windowStart = now.toEpochMilli() - (WINDOW_SECONDS * 1000);
long chunksInWindow = events.stream()
.filter(e -> e.timestamp.toEpochMilli() > windowStart)
.count();
// 计算每秒速率
double ratePerSecond = (double) chunksInWindow / WINDOW_SECONDS;
List<String> reasons = new ArrayList<>();
Stage newStage = Stage.NORMAL;
if (ratePerSecond > BLOCK_THRESHOLD) {
// 严重超限:直接阻断
reasons.add("CRITICAL_RATE: " + String.format("%.1f", ratePerSecond)
+ " chunks/sec (block threshold: " + BLOCK_THRESHOLD + ")");
newStage = Stage.BLOCK;
} else if (ratePerSecond > LIMIT_THRESHOLD) {
// 中度超限:限制加载
reasons.add("HIGH_RATE: " + String.format("%.1f", ratePerSecond)
+ " chunks/sec (limit threshold: " + LIMIT_THRESHOLD + ")");
newStage = Stage.LIMIT;
} else if (ratePerSecond > WARN_THRESHOLD) {
// 轻度超限:警告记录
reasons.add("ELEVATED_RATE: " + String.format("%.1f", ratePerSecond)
+ " chunks/sec (warn threshold: " + WARN_THRESHOLD + ")");
newStage = Stage.WARN;
}
// === 检测3:方向一致性检测(区分正常探索和自动化扫描) ===
// 正常玩家移动有方向惯性,Bot 扫描呈网格状来回跳跃
if (events.size() >= 10) {
double directionChanges = countDirectionChanges(events);
double dirChangeRatio = directionChanges / Math.min(events.size(), 10);
// 如果方向变化率 > 80%,说明是扫描式加载而非线性移动
if (dirChangeRatio > 0.8 && ratePerSecond > WARN_THRESHOLD) {
reasons.add("SCAN_PATTERN: " + String.format("%.0f%%", dirChangeRatio * 100)
+ " direction changes, bot-like scanning detected");
// 扫描模式升级响应等级
if (newStage == Stage.WARN) newStage = Stage.LIMIT;
}
}
// 应用响应阶段
if (newStage != Stage.NORMAL) {
playerStage.put(playerName, newStage);
long cooldownSeconds = switch (newStage) {
case WARN -> COOLDOWN_WARN_SECONDS;
case LIMIT -> COOLDOWN_LIMIT_SECONDS;
case BLOCK -> COOLDOWN_BLOCK_SECONDS;
default -> 0;
};
playerCooldownUntil.put(playerName, Instant.now().plusSeconds(cooldownSeconds));
}
// 返回结果
if (newStage == Stage.BLOCK) {
blockedChunkLoads.incrementAndGet();
return ChunkLimitResult.flagged(reasons);
} else if (newStage == Stage.LIMIT) {
blockedChunkLoads.incrementAndGet();
return ChunkLimitResult.blocked(reasons);
} else if (newStage == Stage.WARN) {
return ChunkLimitResult.suspicious((int) ratePerSecond, reasons);
}
return ChunkLimitResult.clean();
}
/**
* 计算最近区块加载事件的方向变化次数。
* 相邻两次加载的区块坐标变化方向不同计为一次方向变化。
*/
private double countDirectionChanges(List<ChunkLoadEvent> events) {
if (events.size() < 3) return 0;
int changes = 0;
int count = Math.min(events.size(), 12);
int start = Math.max(0, events.size() - count);
for (int i = start + 2; i < events.size(); i++) {
int dx1 = events.get(i - 1).chunkX - events.get(i - 2).chunkX;
int dz1 = events.get(i - 1).chunkZ - events.get(i - 2).chunkZ;
int dx2 = events.get(i).chunkX - events.get(i - 1).chunkX;
int dz2 = events.get(i).chunkZ - events.get(i - 1).chunkZ;
// 方向变化:X 或 Z 方向的正负号发生变化
if (Integer.signum(dx1) != Integer.signum(dx2)
|| Integer.signum(dz1) != Integer.signum(dz2)) {
changes++;
}
}
return changes;
}
/**
* 清除指定玩家的所有追踪数据(玩家退出时调用)。
*/
public void clearPlayer(String playerName) {
playerLoadHistory.remove(playerName);
playerStage.remove(playerName);
playerCooldownUntil.remove(playerName);
extremeCoordEvents.remove(playerName);
}
/**
* 获取当前模块运行状态与统计。
*/
public Map<String, Object> getStatus() {
Map<String, Object> s = new LinkedHashMap<>();
s.put("totalChunkLoads", totalChunkLoads.get());
s.put("blockedChunkLoads", blockedChunkLoads.get());
s.put("chunkBanDetections", chunkBanDetections.get());
s.put("trackedPlayers", playerLoadHistory.size());
s.put("activeStages", playerStage.size());
s.put("enabled", config.getSecurity().getSuperEvolution().isChunkLoadRateLimiter());
// 世界级别统计
Map<String, Object> worldStats = new LinkedHashMap<>();
worldChunkLoadTotal.forEach((world, count) -> worldStats.put(world, count.get()));
s.put("worldStats", worldStats);
// 当前受限玩家列表
List<Map<String, Object>> limitedPlayers = new ArrayList<>();
for (Map.Entry<String, Stage> e : playerStage.entrySet()) {
if (e.getValue() != Stage.NORMAL) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("player", e.getKey());
m.put("stage", e.getValue().name());
Instant cooldown = playerCooldownUntil.get(e.getKey());
m.put("cooldownUntil", cooldown != null ? cooldown.toString() : "N/A");
List<ChunkLoadEvent> events = playerLoadHistory.get(e.getKey());
if (events != null) {
long recent = events.stream()
.filter(ev -> ev.timestamp.isAfter(Instant.now().minusSeconds(WINDOW_SECONDS)))
.count();
m.put("recentLoads", recent);
}
limitedPlayers.add(m);
}
}
s.put("limitedPlayers", limitedPlayers);
// 最近极端坐标事件
List<Map<String, Object>> extremeEvents = new ArrayList<>();
for (Map.Entry<String, List<ExtremeCoordEvent>> e : extremeCoordEvents.entrySet()) {
for (ExtremeCoordEvent ev : e.getValue()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("player", ev.playerName);
m.put("chunkX", ev.chunkX);
m.put("chunkZ", ev.chunkZ);
m.put("world", ev.worldName);
m.put("time", ev.timestamp.toString());
extremeEvents.add(m);
}
}
extremeEvents.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString()));
s.put("extremeCoordEvents", extremeEvents.subList(0, Math.min(extremeEvents.size(), 10)));
return s;
}
public long getTotalChunkLoads() { return totalChunkLoads.get(); }
public long getBlockedChunkLoads() { return blockedChunkLoads.get(); }
public long getChunkBanDetections() { return chunkBanDetections.get(); }
/**
* 定期清理过期记录。
*/
private void cleanupOldRecords() {
Instant cutoff = Instant.now().minusSeconds(RECORD_RETENTION_SECONDS);
playerLoadHistory.entrySet().removeIf(e -> {
List<ChunkLoadEvent> events = e.getValue();
events.removeIf(ev -> ev.timestamp.isBefore(cutoff));
return events.isEmpty();
});
extremeCoordEvents.entrySet().removeIf(e -> {
List<ExtremeCoordEvent> events = e.getValue();
events.removeIf(ev -> ev.timestamp.isBefore(cutoff));
return events.isEmpty();
});
// 清理已过期的冷却
playerCooldownUntil.entrySet().removeIf(e -> e.getValue().isBefore(Instant.now()));
// 同步清理已过期冷却的阶段记录
playerStage.keySet().removeIf(k -> !playerCooldownUntil.containsKey(k));
}
// ==================== 内部数据类 ====================
/**
* 区块加载事件记录。
*/
private static class ChunkLoadEvent {
final Instant timestamp;
final int chunkX;
final int chunkZ;
final String worldName;
ChunkLoadEvent(Instant timestamp, int chunkX, int chunkZ, String worldName) {
this.timestamp = timestamp;
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.worldName = worldName;
}
}
/**
* 极端坐标加载事件记录 — 用于 Chunk Ban 攻击溯源。
*/
private static class ExtremeCoordEvent {
final Instant timestamp;
final String playerName;
final int chunkX;
final int chunkZ;
final String worldName;
ExtremeCoordEvent(Instant timestamp, String playerName, int chunkX, int chunkZ, String worldName) {
this.timestamp = timestamp;
this.playerName = playerName;
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.worldName = worldName;
}
}
// ==================== 检测结果类 ====================
/**
* 区块加载限流检测结果。
*/
public static class ChunkLimitResult {
private final boolean blocked;
private final boolean flagged;
private final boolean suspicious;
private final int score;
private final List<String> reasons;
private ChunkLimitResult(boolean blocked, boolean flagged, boolean suspicious,
int score, List<String> reasons) {
this.blocked = blocked;
this.flagged = flagged;
this.suspicious = suspicious;
this.score = score;
this.reasons = reasons;
}
/** 正常放行 */
public static ChunkLimitResult clean() {
return new ChunkLimitResult(false, false, false, 0, List.of());
}
/** 可疑行为(警告阶段) */
public static ChunkLimitResult suspicious(int score, List<String> reasons) {
return new ChunkLimitResult(false, false, true, score, reasons);
}
/** 阻止加载但不标记玩家 */
public static ChunkLimitResult blocked(List<String> reasons) {
return new ChunkLimitResult(true, false, true, 0, reasons);
}
/** 已标记(严重违规,需要干预) */
public static ChunkLimitResult flagged(List<String> reasons) {
return new ChunkLimitResult(true, true, true, 100, reasons);
}
public boolean isBlocked() { return blocked; }
public boolean isFlagged() { return flagged; }
public boolean isSuspicious() { return suspicious; }
public boolean isClean() { return !blocked && !flagged && !suspicious; }
public int getScore() { return score; }
public List<String> getReasons() { return reasons; }
}
}