安全模块大规模重构:151文件从com.aluer.security分包到8个子包

新包结构:
- com.aluer.anticheat.combat (16) — KillAura/Reach/Criticals/CrystalAura/Totem等战斗反作弊
- com.aluer.anticheat.movement (18) — Fly/Speed/Jesus/NoFall/Timer/Phase等移动反作弊
- com.aluer.anticheat.world (14) — Nuker/Xray/AutoMine/VeinMiner等世界反作弊
- com.aluer.anticheat.player (8) — FakePlayer/AltAccount/NameSpoof等玩家反作弊
- com.aluer.network (32) — DDoS/Protocol/TokenBucket/Firewall等网络安全
- com.aluer.server (10) — ChunkLoad/EntityCount/Redstone/CrashExploit等服务器保护
- com.aluer.chat (5) — ChatFlood/Advertisement/Phishing/CommandAbuse等聊天安全
- com.aluer.defense (50) — WAF/IDS/IPS/Honeypot/EDR/Forensics等纵深防御

所有package声明+跨包import已更新,323测试全绿
This commit is contained in:
Peijun Zhao
2026-05-22 19:29:45 +08:00
parent 5adbbd9592
commit 055de65e88
181 changed files with 682 additions and 303 deletions
@@ -0,0 +1,415 @@
package com.aluer.anticheat.player;
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;
/**
* 背包操作异常检测 (Inventory Manipulation) — V4.0 玩家行为安全模块
*
* 检测原理:
* 部分 Minecraft 外挂客户端利用背包/物品栏操作的漏洞实现非法功能,
* 例如瞬间移动物品、批量丢弃、访问非法槽位等。本模块通过以下维度检测:
* 1. 物品瞬间移动——物品从背包某个槽位移到另一个槽位(或容器)的时间间隔
* < 1 tick(50ms)为异常,正常操作至少需要 1 tick 的服务器处理时间。
* 2. 批量丢弃模式——正常每个 tick 最多丢弃 1 个物品(Q键),单 tick 丢弃
* 超过 1 个物品是发包修改的行为(如 InventoryCleaner)。
* 3. 非法槽位访问——访问负索引槽位或超过背包允许大小的槽位,
* 正常游戏客户端不会发送此类请求。
* 4. 自动整理模式——短时间内大量物品槽位被重新排列,
* 正常玩家整理背包有一定节奏,自动化整理极度快速且统一。
* 5. 物品复制检测——物品从一个槽位移走后,在另一处出现相同物品(可能伴随数量变化)。
*
* 配置开关:serverguard.security.super-evolution.anti-inventory-manipulation
*/
@Service
public class AntiInventoryManipulationService {
private final ServerGuardConfig config;
private final Map<String, List<InventoryOp>> playerInvOps = new ConcurrentHashMap<>();
private final Map<String, Map<Integer, InventorySlotState>> playerSlotStates = new ConcurrentHashMap<>();
private final Map<String, Instant> flaggedPlayers = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
/** 背包操作总次数 */
private final AtomicLong totalInventoryOps = new AtomicLong(0);
/** 背包操作违规次数 */
private final AtomicLong manipulationViolations = new AtomicLong(0);
/** 槽位违规次数 */
private final AtomicLong slotViolations = new AtomicLong(0);
/** 物品移动最小时间间隔(毫秒)——低于此值视为瞬间移动 */
private static final long MIN_MOVE_INTERVAL_MS = 50;
/** 单 tick 最大丢弃物品数 */
private static final int MAX_DROPS_PER_TICK = 1;
/** 玩家背包最小槽位索引 */
private static final int MIN_SLOT_INDEX = 0;
/** 玩家背包最大槽位索引(36 格背包 + 1 副手 = 37 个独立槽位,0-based 为 36) */
private static final int MAX_SLOT_INDEX = 40;
/** 玩家可用的最大槽位索引(含盔甲栏 = 40) */
private static final int MAX_VALID_SLOT = 40;
/** 短时间内大量物品重排的检测窗口(秒) */
private static final long REARRANGE_WINDOW_SEC = 3;
/** 短时间内容器物品变化次数 —— 超过此阈值判定为自动整理 */
private static final int MAX_SLOT_CHANGES_SHORT = 20;
/** 记录保留时间(秒) */
private static final long RECORD_RETENTION_SECONDS = 300;
/** 标记持续时间(秒) */
private static final long FLAG_DURATION_SECONDS = 1800;
public AntiInventoryManipulationService() {
this(new ServerGuardConfig());
}
@Autowired
public AntiInventoryManipulationService(ServerGuardConfig config) {
this.config = config;
scheduler.scheduleAtFixedRate(this::cleanupOldRecords, 60, 120, TimeUnit.SECONDS);
}
/**
* 检测一次物品槽位移动操作。
*
* 追踪物品从一个槽位移动到另一个槽位,检测瞬间移动和非法槽位访问。
*
* @param playerName 玩家名称
* @param playerUUID 玩家 UUID
* @param fromSlot 来源槽位索引
* @param toSlot 目标槽位索引
* @param itemType 移动的物品类型(可为 null)
* @param itemCount 移动的物品数量
* @param containerId 容器 ID(-1 表示玩家自己背包)
* @return 检测结果
*/
public DetectionResult detectItemMove(String playerName, String playerUUID,
int fromSlot, int toSlot, String itemType,
int itemCount, int containerId) {
if (!config.getSecurity().getSuperEvolution().isAntiInventoryManipulation()) {
return DetectionResult.clean();
}
// 检查标记
Instant flaggedUntil = flaggedPlayers.get(playerName);
if (flaggedUntil != null) {
if (Instant.now().isAfter(flaggedUntil)) {
flaggedPlayers.remove(playerName);
} else {
return DetectionResult.flagged(List.of(
"PLAYER_ALREADY_FLAGGED: " + playerName));
}
}
Instant now = Instant.now();
List<InventoryOp> ops = playerInvOps.computeIfAbsent(playerName,
k -> Collections.synchronizedList(new ArrayList<>()));
// 记录操作
InventoryOp op = new InventoryOp(now, "MOVE", fromSlot, toSlot, itemType,
itemCount, containerId);
ops.add(op);
totalInventoryOps.incrementAndGet();
int score = 0;
List<String> reasons = new ArrayList<>();
// === 检测 1: 物品瞬间移动 ===
// 查找同一物品在前一个 tick 内是否有其他移动操作
long tickStart = now.toEpochMilli() - MIN_MOVE_INTERVAL_MS;
long movesInTick = ops.stream()
.filter(o -> "MOVE".equals(o.opType))
.filter(o -> o.time.toEpochMilli() > tickStart)
.count();
if (movesInTick > 2) {
score += 30;
reasons.add("INSTANT_MOVE: " + movesInTick +
" slot moves in " + MIN_MOVE_INTERVAL_MS + "ms (packet exploit)");
}
// === 检测 2: 非法槽位访问 ===
if (fromSlot < MIN_SLOT_INDEX || fromSlot > MAX_SLOT_INDEX
|| toSlot < MIN_SLOT_INDEX || toSlot > MAX_SLOT_INDEX) {
score += 40;
slotViolations.incrementAndGet();
reasons.add("INVALID_SLOT: access slot[" + fromSlot + "->" + toSlot
+ "] (valid range " + MIN_SLOT_INDEX + "-" + MAX_SLOT_INDEX + ")");
}
// === 检测 3: 自动整理模式 ===
// 短时间内容器物品变化次数
long rearrangeWindowStart = now.toEpochMilli() - (REARRANGE_WINDOW_SEC * 1000);
long slotChanges = ops.stream()
.filter(o -> o.time.toEpochMilli() > rearrangeWindowStart)
.count();
if (slotChanges > MAX_SLOT_CHANGES_SHORT) {
score += 25;
reasons.add("AUTO_SORT: " + slotChanges +
" slot changes in " + REARRANGE_WINDOW_SEC + "s (automated inventory sorting)");
}
// === 检测 4: 物品复制检测 ===
// 追踪物品槽位状态变化,检测疑似复制
if (itemType != null && itemCount > 0) {
Map<Integer, InventorySlotState> slotStates = playerSlotStates.computeIfAbsent(playerName,
k -> new ConcurrentHashMap<>());
// 检查来源槽位是否有状态记录
InventorySlotState fromState = slotStates.get(fromSlot);
if (fromState != null && fromState.itemType != null
&& fromState.itemType.equals(itemType)) {
// 物品移出:数量减少是正常的,但数量增加(超过原记录)是异常的
// 这是正常情况下物品在移出(我们会更新状态追踪)
}
// 更新槽位状态
slotStates.put(toSlot, new InventorySlotState(itemType, itemCount, now));
if (fromState != null) {
// 移出后标记为空
slotStates.put(fromSlot, new InventorySlotState("EMPTY", 0, now));
}
}
if (score >= 40) {
flaggedPlayers.put(playerName, Instant.now().plusSeconds(FLAG_DURATION_SECONDS));
manipulationViolations.incrementAndGet();
return DetectionResult.flagged(reasons);
} else if (score >= 20) {
return DetectionResult.suspicious(score, reasons);
}
return DetectionResult.clean();
}
/**
* 检测物品丢弃操作——分析是否使用了批量丢弃外挂。
*
* @param playerName 玩家名称
* @param playerUUID 玩家 UUID
* @param itemType 丢弃的物品类型
* @param itemCount 丢弃数量
* @param slot 丢弃来源槽位
* @return 检测结果
*/
public DetectionResult detectItemDrop(String playerName, String playerUUID,
String itemType, int itemCount, int slot) {
if (!config.getSecurity().getSuperEvolution().isAntiInventoryManipulation()) {
return DetectionResult.clean();
}
// 检查标记
Instant flaggedUntil = flaggedPlayers.get(playerName);
if (flaggedUntil != null) {
if (Instant.now().isAfter(flaggedUntil)) {
flaggedPlayers.remove(playerName);
} else {
return DetectionResult.flagged(List.of("PLAYER_ALREADY_FLAGGED"));
}
}
Instant now = Instant.now();
List<InventoryOp> ops = playerInvOps.computeIfAbsent(playerName,
k -> Collections.synchronizedList(new ArrayList<>()));
// 记录丢弃事件
InventoryOp op = new InventoryOp(now, "DROP", slot, -1, itemType,
itemCount, -1);
ops.add(op);
totalInventoryOps.incrementAndGet();
int score = 0;
List<String> reasons = new ArrayList<>();
// === 批量丢弃检测 ===
// 在 50ms 内丢弃多个物品 = InventoryCleaner 行为
long tickStart = now.toEpochMilli() - 50;
long dropsInTick = ops.stream()
.filter(o -> "DROP".equals(o.opType))
.filter(o -> o.time.toEpochMilli() > tickStart)
.count();
if (dropsInTick > MAX_DROPS_PER_TICK) {
score += 45;
reasons.add("BATCH_DROP: " + dropsInTick +
" items dropped in single tick (InventoryCleaner signature, max normal="
+ MAX_DROPS_PER_TICK + ")");
}
// === 快速连续丢弃 ===
long recentDrops = ops.stream()
.filter(o -> "DROP".equals(o.opType))
.filter(o -> o.time.toEpochMilli() > now.toEpochMilli() - 1000)
.count();
if (recentDrops >= 10) {
score += 20;
reasons.add("RAPID_DROP: " + recentDrops + " items dropped in 1 second");
}
if (score >= 40) {
flaggedPlayers.put(playerName, Instant.now().plusSeconds(FLAG_DURATION_SECONDS));
manipulationViolations.incrementAndGet();
return DetectionResult.flagged(reasons);
} else if (score >= 20) {
return DetectionResult.suspicious(score, reasons);
}
return DetectionResult.clean();
}
/**
* 清除指定玩家的所有追踪数据。
*
* @param playerName 玩家名称
*/
public void clearPlayer(String playerName) {
playerInvOps.remove(playerName);
playerSlotStates.remove(playerName);
flaggedPlayers.remove(playerName);
}
/**
* 获取模块运行状态。
*
* @return 包含统计数据的 LinkedHashMap
*/
public Map<String, Object> getStatus() {
Map<String, Object> s = new LinkedHashMap<>();
s.put("totalInventoryOps", totalInventoryOps.get());
s.put("manipulationViolations", manipulationViolations.get());
s.put("slotViolations", slotViolations.get());
s.put("trackedPlayers", playerInvOps.size());
s.put("flaggedPlayers", flaggedPlayers.size());
s.put("enabled", config.getSecurity().getSuperEvolution().isAntiInventoryManipulation());
List<Map<String, Object>> flagged = new ArrayList<>();
for (Map.Entry<String, Instant> e : flaggedPlayers.entrySet()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("player", e.getKey());
m.put("flaggedUntil", e.getValue().toString());
List<InventoryOp> ops = playerInvOps.get(e.getKey());
if (ops != null) {
m.put("totalOperations", ops.size());
}
flagged.add(m);
}
s.put("flaggedPlayersList", flagged);
return s;
}
public long getTotalInventoryOps() { return totalInventoryOps.get(); }
public long getManipulationViolations() { return manipulationViolations.get(); }
public long getSlotViolations() { return slotViolations.get(); }
/**
* 定期清理过期记录。
*/
private void cleanupOldRecords() {
Instant cutoff = Instant.now().minusSeconds(RECORD_RETENTION_SECONDS);
playerInvOps.entrySet().removeIf(e -> {
List<InventoryOp> ops = e.getValue();
ops.removeIf(o -> o.time.isBefore(cutoff));
return ops.isEmpty();
});
playerSlotStates.entrySet().removeIf(e -> e.getValue().isEmpty());
flaggedPlayers.entrySet().removeIf(e -> e.getValue().isBefore(Instant.now()));
}
// ==================== 内部数据类 ====================
/**
* 背包操作记录——记录每次物品移动/丢弃等操作。
*/
private static class InventoryOp {
final Instant time;
final String opType; // "MOVE" / "DROP"
final int fromSlot;
final int toSlot; // -1 for drops
final String itemType;
final int itemCount;
final int containerId;
InventoryOp(Instant time, String opType, int fromSlot, int toSlot,
String itemType, int itemCount, int containerId) {
this.time = time;
this.opType = opType;
this.fromSlot = fromSlot;
this.toSlot = toSlot;
this.itemType = itemType;
this.itemCount = itemCount;
this.containerId = containerId;
}
}
/**
* 背包槽位状态快照——追踪每个槽位的内容变化。
*/
private static class InventorySlotState {
final String itemType;
final int itemCount;
final Instant snapshotTime;
InventorySlotState(String itemType, int itemCount, Instant snapshotTime) {
this.itemType = itemType;
this.itemCount = itemCount;
this.snapshotTime = snapshotTime;
}
}
// ==================== 检测结果类 ====================
/**
* 背包操作异常检测结果。
*/
public static class DetectionResult {
private final boolean flagged;
private final boolean suspicious;
private final int score;
private final List<String> reasons;
private DetectionResult(boolean flagged, boolean suspicious, int score, List<String> reasons) {
this.flagged = flagged;
this.suspicious = suspicious;
this.score = score;
this.reasons = reasons;
}
/** 无异常:背包操作正常 */
public static DetectionResult clean() {
return new DetectionResult(false, false, 0, List.of());
}
/** 可疑行为:存在异常背包操作模式 */
public static DetectionResult suspicious(int score, List<String> reasons) {
return new DetectionResult(false, true, score, reasons);
}
/** 已标记:检测到高置信度背包操作异常 */
public static DetectionResult flagged(List<String> reasons) {
return new DetectionResult(true, true, 100, reasons);
}
public boolean isFlagged() { return flagged; }
public boolean isSuspicious() { return suspicious; }
public boolean isClean() { return !flagged && !suspicious; }
public int getScore() { return score; }
public List<String> getReasons() { return reasons; }
}
}