第48批(1/2):修复对账指纹未落盘致网页改密被反向覆盖;登录失败自动对账重试;绑定2FA后游戏登录强制密码+动态码两关
Check Source / check (push) Successful in 5s
Auto Release / build (push) Successful in 2m24s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s

- saveAlignSeen 自杀式节流修复:原节流条件 now-dirtyAt<5000 恒成立(调用点先设 dirtyAt 再调用)
  → align_seen.json 从未落盘 → 重启后 seen 全空 → 网页新密码被游戏旧密码反向覆盖;
  改为以「上次成功写入」alignSeenLastWrite 为基准节流
- /login 密码不匹配:15秒/人限流 → 异步 pushWebLoginCredentials(含 alignGate 拉回) →
  主线程自动重校验;重试仍败走正常风控计数(解决网页改密后游戏内密码对不上)
- 已绑定2FA(twofa_enabled=1)的玩家密码通过后不再直接放行:requireTwoFaIfEnabled
  转入双验证第二步等 /2fa code(此前仅 IP 变更才触发两关,同IP登录第一层过关直接放行)
This commit is contained in:
youpaishidifu
2026-10-23 11:27:38 +08:00
parent 2090939e87
commit 7525b8f6b5
2 changed files with 83 additions and 2 deletions
@@ -12,6 +12,13 @@ public class LoginManager {
this.plugin = plugin;
}
/** ★第48批#244:/login 失败自动对账重试的每人限流(值=上次对账触发时刻) */
private final java.util.Map<String, Long> pwSyncRetryAt =
new java.util.concurrent.ConcurrentHashMap<>();
/** ★第48批#244:正在对账重试中的玩家(防递归) */
private final java.util.Set<String> pwSyncRetrying =
java.util.concurrent.ConcurrentHashMap.newKeySet();
public boolean handleRegister(Player p,
String[] args) {
String name = p.getName();
@@ -191,6 +198,8 @@ public class LoginManager {
if (matchMain) {
// ★ IP风控双验证第一步:只认密码,不解冻、不恢复背包
if (markDualStep1(p, false)) return true;
// ★第48批#249:已绑定2FA → 密码过后强制第二关 /2fa code
if (requireTwoFaIfEnabled(p, false)) return true;
finishPasswordLogin(p, false);
return true;
}
@@ -202,11 +211,17 @@ public class LoginManager {
if (pwdResult != null) {
// ★ IP风控双验证第一步:只认密码,不解冻、不恢复背包
if (markDualStep1(p, true)) return true;
// ★第48批#249:已绑定2FA → 密码过后强制第二关 /2fa code
if (requireTwoFaIfEnabled(p, true)) return true;
finishPasswordLogin(p, true);
return true;
}
// ★第48批#244:密码不匹配 → 先尝试与网页端即时对账(网页可能刚改密被覆盖),
// 15秒/人限流;对账完成后主线程自动重校验,仍未过才计失败次数
if (maybeRetryAfterWebSync(p, pwd)) return true;
// ★ 密码错误,记录风控
int remaining = plugin.getRiskControl()
.recordFailAttempt(p);
@@ -251,6 +266,67 @@ public class LoginManager {
return true;
}
/**
* ★第48批#249:已绑定 2FA(twofa_enabled=1)的玩家,密码通过后不直接放行,
* 转入「密码 + 2FA」双验证第二步(等 /2fa code)。
* 仅在该玩家当前不在双验证流程时生效(IP风控已 mark 的走 markDualStep1)。
*
* @return true = 已进入双验证流程,本轮 /login 到此为止
*/
private boolean requireTwoFaIfEnabled(Player p, boolean tempPassword) {
TwoFactorManager twofa = plugin.getTwofa();
String name = p.getName();
if (twofa == null) return false;
if (twofa.isDualVerify(p)) return false; // IP风控路径已处理
if (!plugin.getDb().userExists(name)) return false;
if (!twofa.isEnabled(name)) return false; // 未绑定2FA:维持原单步登录
// 密码这一步确实对了 → 建立双验证状态并记录进度(不解冻、不恢复背包)
twofa.markDualVerify(p, plugin.getPlayerIP(p));
twofa.onDualPasswordPassed(p, tempPassword);
// 重置「密码错误次数」风控(密码对了,但仍未完成登录)
plugin.getRiskControl().onLoginSuccess(name);
p.sendMessage("§a§l[双重验证] §a第一步密码验证通过(尚未解冻)");
p.sendMessage("§7第二步: 执行 §f/2fa code <动态码|恢复代码>");
p.sendMessage("§c两步全部通过后才会解冻并恢复背包");
return true;
}
/**
* ★第48批#244:密码校验失败时先与网页端对账(拉回网页新密码)再自动重试一次。
* 15秒/人限流;重试仍失败走正常风控计数。
*
* @return true = 本轮 /login 已交给异步对账流程处理(调用方直接 return)
*/
private boolean maybeRetryAfterWebSync(Player p, String pwd) {
if (plugin.webManager == null) return false;
String name = p.getName();
if (pwSyncRetrying.contains(name)) return false; // 重试轮失败:不再触发
long now = System.currentTimeMillis();
Long last = pwSyncRetryAt.get(name);
if (last != null && now - last < 15000L) return false; // 15秒/人限流
pwSyncRetryAt.put(name, now);
pwSyncRetrying.add(name);
p.sendMessage("§e§l[登录] §e正在核对网页端最新密码…");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
plugin.webManager.pushWebLoginCredentials(); // 含 alignGate 拉回
} catch (Throwable t) {
plugin.getLogger().warning("[登录] 对账重试同步异常: " + t.getMessage());
}
Bukkit.getScheduler().runTask(plugin, () -> {
try {
Player online = Bukkit.getPlayerExact(name);
if (online == null || !online.isOnline()) return;
// 主线程重新走一次完整登录校验(含2FA/风控前置)
handleLogin(online, new String[]{pwd});
} finally {
pwSyncRetrying.remove(name);
}
});
});
return true;
}
/**
* 密码登录成功的统一收口:解冻 + 恢复背包 + 记录登录信息。
* 调用点:/login 主密码、/login 临时密码、IP风控双验证第二步通过后。
+7 -2
View File
@@ -4513,8 +4513,13 @@ public class WebManager {
private void saveAlignSeen(boolean force) {
long now = System.currentTimeMillis();
if (!force) {
if (alignSeenDirtyAt == 0 || now - alignSeenDirtyAt < 5000) return;
if (now - alignSeenLastWrite < 5000) return;
// ★第48批根因修复:原写法 now - alignSeenDirtyAt < 5000 恒成立(调用点
// 先把 dirtyAt 设为 now 再调本方法)→ 永远 return,文件从未落盘,
// 重启后 seen 全空 → 网页新密码被游戏旧密码反向覆盖。
// 改为:无待写变更才短路;节流以「上次成功写入」为基准。
if (alignSeenDirtyAt == 0) return;
if (alignSeenLastWrite != 0
&& now - alignSeenLastWrite < 5000) return;
}
alignSeenLastWrite = now;
alignSeenDirtyAt = 0;