第57批续10:webbind绑邮箱密码验证接入统一风控+空行清理
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m21s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

- Main.java: /webbind approve 密码验证从独立webbindPwdFails内存计数器
  接入RiskControlManager(与/login完全一致):密码错5次→Bukkit真实封禁
  5分钟+踢出+报警邮件+重连拒绝+到期自动解封
- Main.java: 删除已无引用的webbindPwdFails字段
- RiskControlManager.java: 按8a693e规则清理方法内部空行(11行)
This commit is contained in:
youpaishidifu
2026-10-26 19:20:05 +08:00
parent 74e2c60d60
commit 325d363f4b
2 changed files with 24 additions and 26 deletions
+24 -15
View File
@@ -142,7 +142,6 @@ public class Main extends JavaPlugin
new HashMap<>(); new HashMap<>();
private static final long IP_SKIP_MS = 300000L; private static final long IP_SKIP_MS = 300000L;
// ★ 第56批:/webbind 批准密码验证失败计数(玩家名 -> 失败次数) // ★ 第56批:/webbind 批准密码验证失败计数(玩家名 -> 失败次数)
private final ConcurrentHashMap<String, Integer> webbindPwdFails = new ConcurrentHashMap<>();
// ★ 第57批续5: /webbind approve 改聊天框输入密码(玩家名 -> 请求ID) // ★ 第57批续5: /webbind approve 改聊天框输入密码(玩家名 -> 请求ID)
private final ConcurrentHashMap<String, String> pendingWebbindApprove = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, String> pendingWebbindApprove = new ConcurrentHashMap<>();
private SalesStatsManager salesStats; private SalesStatsManager salesStats;
@@ -6271,28 +6270,36 @@ public class Main extends JavaPlugin
"reject", "", status -> {}); "reject", "", status -> {});
return; return;
} }
// 密码验证 // ★ 第57批续10:接入统一风控(RiskControlManager),与 /login 完全一致
int failCount2 = webbindPwdFails.getOrDefault(p.getName(), 0); // 密码错5次 → Bukkit真实封禁5分钟 + 踢出 + 报警邮件(不再仅提示)
if (failCount2 >= 5) { if (riskControl != null && riskControl.isBanned(p.getName())) {
int rem2 = riskControl.getBanRemainingSeconds(p.getName());
pendingWebbindApprove.remove(p.getName()); pendingWebbindApprove.remove(p.getName());
p.sendMessage("§c§l[风控] §f连续5次失败,已被临时限制"); p.sendMessage("§c§l[风控] §f密码错误次数过多,账号已被临时封禁 §e"
+ rem2 + " §f秒");
return; return;
} }
String pwdSalt2 = (String) db.getField(p.getName(), "password_salt"); String pwdSalt2 = (String) db.getField(p.getName(), "password_salt");
String pwdHash2 = PasswordUtils.hash(input, pwdSalt2); String pwdHash2 = PasswordUtils.hash(input, pwdSalt2);
String pwdResult2 = db.checkPasswordWithFallback(p.getName(), pwdHash2); String pwdResult2 = db.checkPasswordWithFallback(p.getName(), pwdHash2);
if (pwdResult2 == null) { if (pwdResult2 == null) {
failCount2++; int remaining2 = riskControl != null
webbindPwdFails.put(p.getName(), failCount2); ? riskControl.recordFailAttempt(p) : 4;
p.sendMessage("§c密码错误(" + failCount2 + "/5)"); if (remaining2 < 0) {
if (failCount2 >= 5) { // 已达阈值:triggerBan 已真实封禁并踢出
p.sendMessage("§c§l[风控] §f连续5次失败,已被临时限制"); return;
}
p.sendMessage("§c密码错误");
if (remaining2 > 0) {
p.sendMessage("§c§l[风控] §f剩余 §e" + remaining2 + " §f次尝试机会");
} else {
p.sendMessage("§c§l[风控] §f连续5次失败,已被临时封禁");
pendingWebbindApprove.remove(p.getName()); pendingWebbindApprove.remove(p.getName());
} }
return; return;
} }
// 验证通过,清零计数 // 验证通过,重置风控计数
webbindPwdFails.remove(p.getName()); if (riskControl != null) riskControl.onLoginSuccess(p.getName());
String reqId2 = pendingBindReq; String reqId2 = pendingBindReq;
pendingWebbindApprove.remove(p.getName()); pendingWebbindApprove.remove(p.getName());
p.sendMessage("§e正在提交批准…"); p.sendMessage("§e正在提交批准…");
@@ -9710,9 +9717,11 @@ public class Main extends JavaPlugin
// ★ 第56批:approve 时需要密码(Java端直接验证,不再依赖PHP复核) // ★ 第56批:approve 时需要密码(Java端直接验证,不再依赖PHP复核)
if (bindApprove) { if (bindApprove) {
// ★ 第57批续5: 改聊天框输入密码(不再要求命令行参数密码) // ★ 第57批续5: 改聊天框输入密码(不再要求命令行参数密码)
int failCount = webbindPwdFails.getOrDefault(wp.getName(), 0); // ★ 第57批续10:接入统一风控,封禁期内直接拒绝
if (failCount >= 5) { if (riskControl != null && riskControl.isBanned(wp.getName())) {
wp.sendMessage("§c§l[风控] §f密码验证失败次数过多,已被临时限制。请稍后再试"); int rem = riskControl.getBanRemainingSeconds(wp.getName());
wp.sendMessage("§c§l[风控] §f密码错误次数过多,账号已被临时封禁 §e"
+ rem + " §f秒,请稍后再试");
return true; return true;
} }
pendingWebbindApprove.put(wp.getName(), bindReqId); pendingWebbindApprove.put(wp.getName(), bindReqId);
@@ -88,23 +88,18 @@ public class RiskControlManager {
public int recordFailAttempt(Player player) { public int recordFailAttempt(Player player) {
String name = player.getName(); String name = player.getName();
int maxAttempts = getMaxFailAttempts(); int maxAttempts = getMaxFailAttempts();
// 已被封禁,直接返回 // 已被封禁,直接返回
if (isBanned(name)) { if (isBanned(name)) {
return -1; return -1;
} }
int current = failAttempts int current = failAttempts
.getOrDefault(name, 0) + 1; .getOrDefault(name, 0) + 1;
failAttempts.put(name, current); failAttempts.put(name, current);
int remaining = Math.max(0, maxAttempts - current); int remaining = Math.max(0, maxAttempts - current);
// 达到阈值 → 触发封禁 // 达到阈值 → 触发封禁
if (current >= maxAttempts) { if (current >= maxAttempts) {
triggerBan(player); triggerBan(player);
} }
return remaining; return remaining;
} }
@@ -117,16 +112,13 @@ public class RiskControlManager {
long durationMs = getBanDurationMs(); long durationMs = getBanDurationMs();
long until = System.currentTimeMillis() + durationMs; long until = System.currentTimeMillis() + durationMs;
banUntil.put(name, until); banUntil.put(name, until);
String ip = plugin.getPlayerIP(player); String ip = plugin.getPlayerIP(player);
int banMinutes = (int) (durationMs / 60000); int banMinutes = (int) (durationMs / 60000);
plugin.getLogger().warning( plugin.getLogger().warning(
"[风控] 玩家 " + name "[风控] 玩家 " + name
+ " 密码错误达到阈值(" + " 密码错误达到阈值("
+ getMaxFailAttempts() + "次)" + getMaxFailAttempts() + "次)"
+ ",封禁 " + banMinutes + " 分钟"); + ",封禁 " + banMinutes + " 分钟");
// ★ 真实封禁:调用Bukkit BanList API阻止玩家重新加入服务器 // ★ 真实封禁:调用Bukkit BanList API阻止玩家重新加入服务器
String banReason = "§c§l[安全风控] 密码连续错误" String banReason = "§c§l[安全风控] 密码连续错误"
+ getMaxFailAttempts() + "次," + getMaxFailAttempts() + "次,"
@@ -145,7 +137,6 @@ public class RiskControlManager {
} }
// 踢出玩家(无论BanList是否成功,立即踢出) // 踢出玩家(无论BanList是否成功,立即踢出)
player.kickPlayer(banReason); player.kickPlayer(banReason);
// 异步发送报警邮件 // 异步发送报警邮件
sendBanAlertEmail(name, ip, banMinutes); sendBanAlertEmail(name, ip, banMinutes);
} }
@@ -164,7 +155,6 @@ public class RiskControlManager {
+ " 未绑定邮箱,跳过报警邮件"); + " 未绑定邮箱,跳过报警邮件");
return; return;
} }
String subject = String subject =
"[安全警告] 您的账号已被临时封禁"; "[安全警告] 您的账号已被临时封禁";
String body = "尊敬的玩家 " + playerName + ":\n\n" String body = "尊敬的玩家 " + playerName + ":\n\n"
@@ -187,7 +177,6 @@ public class RiskControlManager {
+ "如果非本人操作,建议您立即修改密码" + "如果非本人操作,建议您立即修改密码"
+ "(使用 /sdf1_login pw)。\n\n" + "(使用 /sdf1_login pw)。\n\n"
+ "--Sdf1_login 安全系统"; + "--Sdf1_login 安全系统";
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try { try {
plugin.getEmail().sendBody( plugin.getEmail().sendBody(