Files
Aluer/src/main/java/com/aluer/anticheat/player/AntiInventoryManipulationService.java
T
Peijun Zhao 055de65e88 安全模块大规模重构: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测试全绿
2026-05-22 19:29:45 +08:00

416 lines
16 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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; }
}
}