fix: 观赛退出绝不回创造模式、正版验证轮询fail-closed、验证码改为99以内小学加减乘除
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m44s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s

- PVPArenaManager: 进观赛时创造/观察者一律归一为生存再备份,exitSpectator/forceLeaveArena 兜底还原绝不给 CREATIVE(防退出观赛变创造作弊)
- WebManager: 正版验证轮询fail-closed——非JSON/缺字段视为无效响应仅记日志绝不放行,PHP明确失败即终止轮询,verified须凭证齐全且会话归属玩家一致才 autoLogin
- ChatFilterManager: 验证码算术题改为小学水平99以内加减乘除(和/积≤99、减法不为负、除法整除)
This commit is contained in:
youpaishidifu
2026-09-27 23:57:32 +08:00
parent 8388d68cb7
commit 233060e7bd
3 changed files with 94 additions and 24 deletions
+20 -16
View File
@@ -109,7 +109,7 @@ public class ChatFilterManager {
static class VerificationData {
long createTime;
int a, b, op; // 0=加法, 1=减法, 2=乘法(九九表), 3=除法(九九表)
int a, b, op; // 0=加法, 1=减法, 2=乘法, 3=除法(均为99以内小学运算)
String guiTargets; // GUI模式下需要选择的物品名列表("|"分隔)
VerificationType type; // 验证码类型
boolean completed;
@@ -188,7 +188,7 @@ public class ChatFilterManager {
/**
* 生成随机验证码:加 / 减 / 乘 / 除 / GUI 五种每次随机抽一种。
* 乘法与除法严格取自九九乘法表。
* ★ 算术题统一为小学水平「99以内加减乘除」(和/积≤99、减法不为负、除法整除)。
*
* ★ 出题前必须先关掉可能还开着的旧验证码GUI:旧GUI 的 InventoryCloseEvent
* 会清 verificationData,让它先跑完,否则刚生成的新题会被清掉(点哪儿都没用)。
@@ -237,26 +237,30 @@ public class ChatFilterManager {
}
}
/** 生成数学题验证码(op: 0加 1减 2乘 3除;乘/除每次从九九乘法表随机抽一题) */
/**
* 生成数学题验证码(op: 0加 1减 2乘 3除)
* ★ 小学水平「99以内加减乘除」:加法和≤99、减法不出现负数、乘法积≤99、除法整除且被除数≤99
*/
private void generateMathChallenge(String playerName, int op, Random rand) {
int a, b;
switch (op) {
case 0: // 两位数加法
a = 10 + rand.nextInt(90);
b = 10 + rand.nextInt(90);
case 0: // 加法:两数之和不超过99
a = 1 + rand.nextInt(98); // a ∈ [1, 98]
b = 1 + rand.nextInt(99 - a); // b ∈ [1, 99-a] → a+b ≤ 99
break;
case 1: // 两位数减法(保证结果仍是两位数,不出现负数)
a = 20 + rand.nextInt(80);
b = 10 + rand.nextInt(a - 19); // b ∈ [10, a-10],结果 ≥ 10
case 1: // 减法:被减数不超过99,结果≥1(小学不出现负数/零)
a = 2 + rand.nextInt(98); // a ∈ [2, 99]
b = 1 + rand.nextInt(a - 1); // b ∈ [1, a-1] → 结果 ∈ [1, a-1]
break;
case 2: // 九九乘法表:1×1 ~ 9×9
a = 1 + rand.nextInt(9);
b = 1 + rand.nextInt(9);
case 2: // 乘法:积不超过99(如 12×7=84、11×9=99)
b = 2 + rand.nextInt(8); // b ∈ [2, 9]
a = 2 + rand.nextInt((99 / b) - 1); // a ∈ [2, floor(99/b)] → a*b ≤ 99
break;
default: // 九九乘法表除法(整除):(i×j) ÷ j = i
b = 1 + rand.nextInt(9);
a = (1 + rand.nextInt(9)) * b;
default: // 除法:整除且被除数不超过99(如 96÷8=12、99÷9=11)
b = 2 + rand.nextInt(8); // b ∈ [2, 9]
int q = 2 + rand.nextInt((99 / b) - 1); // 商 ∈ [2, floor(99/b)]
a = b * q; // a ≤ 99
break;
}
@@ -484,7 +488,7 @@ public class ChatFilterManager {
case 0 -> expected = vd.a + vd.b;
case 1 -> expected = vd.a - vd.b;
case 2 -> expected = vd.a * vd.b;
default -> expected = vd.a / vd.b; // 九九表整除
default -> expected = vd.a / vd.b; // 生成时保证整除(a % b == 0)
}
if (ans == expected) {
+14 -3
View File
@@ -1235,7 +1235,10 @@ public class PVPArenaManager implements Listener {
GameMode backMode = spectatorBackups.remove(playerName);
if (backMode != null && !isDisconnect && player.isOnline()
&& player.getGameMode() == GameMode.SPECTATOR) {
player.setGameMode(backMode == GameMode.SPECTATOR ? GameMode.SURVIVAL : backMode);
// ★ 兜底:还原目标为观察者或创造时一律回生存(退出观赛绝不回创造,防止作弊)
GameMode restore = (backMode == GameMode.SPECTATOR
|| backMode == GameMode.CREATIVE) ? GameMode.SURVIVAL : backMode;
player.setGameMode(restore);
}
// 清理所有状态(含取消超时定时器)
@@ -2850,7 +2853,12 @@ public class PVPArenaManager implements Listener {
// 2) 记录原模式并进入 MC 原版观察者
// ★ 必须【先记再切】:观察者模式下 addItem 会掉地上,故不再发放任何物品
spectatorBackups.put(p.getName(), p.getGameMode());
// ★ 绝不把【创造模式】当作还原目标:以创造进入观赛的,退出一律回生存(防止退出观赛变成创造作弊)
GameMode before = p.getGameMode();
if (before == GameMode.CREATIVE || before == GameMode.SPECTATOR) {
before = GameMode.SURVIVAL;
}
spectatorBackups.put(p.getName(), before);
p.setGameMode(GameMode.SPECTATOR);
p.sendMessage("§a§l[PVP观赛] §f已进入观察者模式,可穿墙自由观赛");
@@ -2866,7 +2874,10 @@ public class PVPArenaManager implements Listener {
if (p.getGameMode() != GameMode.SPECTATOR) return;
GameMode back = spectatorBackups.remove(p.getName());
if (back == null || back == GameMode.SPECTATOR) back = GameMode.SURVIVAL;
// ★ 兜底:备份缺失/观察者/创造 一律还原为生存(退出观赛绝不回创造,防止作弊)
if (back == null || back == GameMode.SPECTATOR || back == GameMode.CREATIVE) {
back = GameMode.SURVIVAL;
}
// 观察者常悬在高空或钻进方块里 → 切回可玩模式前先挪到可站立的空气格
safeRecoverPosition(p);
+60 -5
View File
@@ -5215,6 +5215,8 @@ public class WebManager {
}
final String[] pollData = {sessionId};
// ★ 连续无效响应(非JSON/结构不符)计数:只用于日志与提示,任何情况下都不会据此放行
final int[] invalidPolls = {0};
final long startTime = System.currentTimeMillis();
final long maxPollTime = 660000; // 11分钟(比会话过期多1分钟)
@@ -5262,16 +5264,69 @@ public class WebManager {
String response = doGet(url);
if (response == null) return;
com.google.gson.JsonObject json = com.google.gson.JsonParser.parseString(response).getAsJsonObject();
if (!json.get("success").getAsBoolean()) return;
// ★ fail-closed:非JSON / 结构不符 一律视为"无效响应",绝不判为通过
com.google.gson.JsonObject json;
try {
json = com.google.gson.JsonParser.parseString(response).getAsJsonObject();
} catch (Exception parseEx) {
invalidPolls[0]++;
if (invalidPolls[0] == 1 || invalidPolls[0] % 15 == 0) {
String preview = response.length() > 200 ? response.substring(0, 200) + "..." : response;
plugin.getLogger().warning("[正版验证] 响应非JSON,忽略(第" + invalidPolls[0] + "次): " + preview);
}
if (invalidPolls[0] == 5) {
Bukkit.getScheduler().runTask(plugin, () -> {
if (player.isOnline()) {
player.sendMessage("§c验证服务响应异常,正在自动重试…");
}
});
}
return;
}
if (!json.has("success") || !json.get("success").isJsonPrimitive()
|| !json.get("success").getAsBoolean()) {
// PHP明确返回失败(会话不存在/密钥错误等结构性错误)→ 终止轮询,不放行
String err = (json.has("error") && json.get("error").isJsonPrimitive())
? json.get("error").getAsString() : "未知错误";
plugin.getLogger().warning("[正版验证] PHP返回失败,终止轮询: " + err);
minecraftAuthSessions.remove(playerName);
minecraftAuthPollers.remove(playerName);
Bukkit.getScheduler().runTask(plugin, () -> {
if (player.isOnline()) {
player.sendMessage("§c验证失败: " + err);
}
});
this.cancel();
return;
}
if (!json.has("data") || !json.get("data").isJsonObject()) {
invalidPolls[0]++;
plugin.getLogger().warning("[正版验证] 响应缺少data,忽略(第" + invalidPolls[0] + "次)");
return;
}
com.google.gson.JsonObject data = json.getAsJsonObject("data");
if (!data.has("status") || !data.get("status").isJsonPrimitive()) {
invalidPolls[0]++;
plugin.getLogger().warning("[正版验证] 响应缺少status,忽略(第" + invalidPolls[0] + "次)");
return;
}
String status = data.get("status").getAsString();
invalidPolls[0] = 0; // 收到有效响应 → 重置无效计数
if ("verified".equals(status)) {
// 验证成功!
String mcUuid = data.get("mc_uuid").getAsString();
String mcUsername = data.get("mc_username").getAsString();
String mcUuid = data.has("mc_uuid") ? data.get("mc_uuid").getAsString() : "";
String mcUsername = data.has("mc_username") ? data.get("mc_username").getAsString() : "";
String sessionPlayer = data.has("player_name") ? data.get("player_name").getAsString() : "";
// ★ fail-closed:凭证不全 / 会话归属玩家不符 → 一律不放行(继续轮询直到超时)
if (mcUuid.isEmpty() || mcUsername.isEmpty()
|| (sessionPlayer != null && !sessionPlayer.isEmpty()
&& !sessionPlayer.equalsIgnoreCase(playerName))) {
plugin.getLogger().warning("[正版验证] verified但校验不通过,拒绝放行: session.player="
+ sessionPlayer + ", uuid=" + mcUuid + ", name=" + mcUsername);
return;
}
plugin.getLogger().info("[正版验证] 验证成功: " + playerName + " -> " + mcUsername + " (" + mcUuid + ")");