Files
Sdf1_login/src/main/java/Sdf1_login/LoginManager.java
T
youpaishidifu 8a693e6417
Check Source / check (push) Successful in 5s
Auto Release / build (push) Successful in 3m0s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
第57批续:封禁播报时间戳文件+空行清理+补丁去重
- BanBroadcastManager:resolveStartCutoff 优先读数据目录10位时间戳文件,无则回退目录创建时间+180天保护;启动时自动创建时间戳文件
- WebManager:删除2075行空行+激进清理(方法内部空行全删,块注释/方法间空行保留);bans_intercept/post_scan去重
- Main/LoginManager:去重重复插入的confirm/cancel ban块和pushLoginVerified块
2026-10-26 00:10:06 +08:00

479 lines
20 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 Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
public class LoginManager {
private final Main plugin;
public LoginManager(Main plugin) {
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();
// 已注册玩家不能再注册
if (plugin.getDb().userExists(name)) {
p.sendMessage(plugin.getConfig2()
.msg("already_registered"));
return true;
}
// IP注册限制检查
String ip = plugin.getPlayerIP(p);
if (ip != null && plugin.getConfig2()
.maxAccountsPerIP > 0) {
if (!plugin.getIPGroup()
.canRegister(ip)) {
int count = plugin.getIPGroup()
.getAccountCount(ip);
int max = plugin.getConfig2()
.maxAccountsPerIP;
p.sendMessage("§c该IP注册已达上限("
+ count + "/" + max + ")");
return true;
}
}
if (args.length < 1) {
p.sendMessage("§c用法: /reg <密码>");
p.sendMessage("§7密码要求: 6位以上,"
+ "包含大小写字母和数字");
return true;
}
String pwd = args[0];
if (pwd.length() < 6) {
p.sendMessage("§c密码长度不足6位");
return true;
}
if (!pwd.matches(".*[a-z].*")
|| !pwd.matches(".*[A-Z].*")
|| !pwd.matches(".*[0-9].*")) {
p.sendMessage("§c密码必须包含大写字母、"
+ "小写字母和数字");
return true;
}
String salt =
PasswordUtils.generateSalt();
String hash =
PasswordUtils.hash(pwd, salt);
plugin.getDb().createUser(
name, hash, salt);
plugin.getLoggedIn().add(name);
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
if (plugin.webManager != null) {
plugin.webManager.recordJavaLogin(name, plugin.getPlayerIP(p));
// ★ 第57批:/login 成功后推送 login_verified=1 给 PHP
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
plugin.webManager.pushLoginVerified(name);
});
}
p.setAllowFlight(false);
p.setFlying(false);
plugin.getDb().setLoggedIn(name, true);
plugin.getDb().setField(name,
"last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name,
"last_online_check",
System.currentTimeMillis());
plugin.getDb().recordIP(name, ip);
plugin.getDb().setField(name,
"register_time",
System.currentTimeMillis());
// ★ 二十二批:DB 读挪异步线程,菜单雪球延后到恢复完成回调
plugin.restoreInventory(p,
() -> plugin.giveMenuSnowball(p));
plugin.recordIPLogin(p);
p.sendMessage(plugin.getConfig2()
.msg("reg_success"));
playRegisterSound(p);
// ★ 注册成功:actionbar 提示1分钟保存刚才的账号和密码
plugin.showCredentialSaveReminder(p, name, pwd);
try {
org.bukkit.plugin.Plugin cy =
Bukkit.getPluginManager()
.getPlugin("CY_beibao");
if (cy != null && cy.isEnabled()) {
cy.getClass().getMethod(
"onSdf1Activation",
String.class,
int.class, int.class)
.invoke(cy, name, 9, 0);
}
} catch (Exception ignored) {
}
// ★ 注册后立即同步用户数据到PHP
if (plugin.webManager != null) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try { plugin.webManager.syncUserRegistrations(); }
catch (Exception e) {
plugin.getLogger().warning("[Web通信] 注册后同步用户异常: " + e.getMessage());
}
});
}
return true;
}
private void playRegisterSound(Player p) {
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_CHIME,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_BASS,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_PLING,
1.0f, 0.79f);
}
public boolean handleLogin(Player p,
String[] args) {
String name = p.getName();
// ★ 在 handleLogin 方法开头
if (plugin.getNeedsPasswordChange().contains(name)
&& plugin.getLoggedIn().contains(name)) {
// 已登录但还没改密码,只允许执行 /sdf1_login pw
p.sendMessage("§c请先修改密码: /sdf1_login pw <旧密码> <新密码>");
return true;
}
try {
if (plugin.getLoggedIn().contains(name)) {
p.sendMessage(plugin.getConfig2()
.msg("already_logged_in"));
return true;
}
if (!plugin.getDb().userExists(name)) {
p.sendMessage(plugin.getConfig2()
.msg("not_registered"));
return true;
}
// ★ 风控检查:封禁期内拒绝登录
if (plugin.getRiskControl().isBanned(name)) {
int remaining = plugin.getRiskControl()
.getBanRemainingSeconds(name);
p.sendMessage("§c§l[安全风控] §f账号因密码错误过多已被封禁,"
+ "剩余 §e" + remaining + " §f秒");
return true;
}
if (args.length < 1) {
p.sendMessage("§c用法: /login <密码>");
return true;
}
String pwd = args[0];
String salt = (String) plugin.getDb()
.getField(name, "password_salt");
if (salt == null) {
p.sendMessage(
"§c数据异常,请联系管理员");
plugin.getLogger().severe(
"[Sdf1_login] 玩家 " + name
+ " 盐值为null");
return true;
}
String hash =
PasswordUtils.hash(pwd, salt);
boolean matchMain =
plugin.getDb().checkPassword(
name, hash);
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;
}
String pwdResult =
plugin.getDb()
.checkPasswordWithFallback(
name, hash);
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;
}
// ★第54批:密码不匹配 → 先同步账号数据(拉回PHP侧凭证入库为主密码),
// 15秒/人限流;同步完成后主线程自动重校验(以Java本地login.db为准),仍未过才计失败次数
if (maybeRetryAfterWebSync(p, pwd)) return true;
// ★ 密码错误,记录风控
int remaining = plugin.getRiskControl()
.recordFailAttempt(p);
if (remaining < 0) {
// 已被封禁(triggerBan内部已踢出)
return true;
}
p.sendMessage(plugin.getConfig2()
.msg("login_failed"));
if (remaining > 0) {
p.sendMessage("§c§l[风控] §f剩余 §e"
+ remaining + " §f次尝试机会");
}
} catch (Exception e) {
plugin.getLogger().severe(
"[Sdf1_login] handleLogin异常: "
+ e.getMessage());
e.printStackTrace();
p.sendMessage(
"§c登录过程出错,请联系管理员");
}
return true;
}
/**
* IP风控「密码 + 2FA」顺序两关的第一步。
* 密码正确只记录进度:<b>不加入 loggedIn、不恢复背包</b>,必须等第二步 /2fa code 通过才收口。
*
* @return true = 该玩家处于双验证流程,本轮 /login 到此为止
*/
private boolean markDualStep1(Player p, boolean tempPassword) {
TwoFactorManager twofa = plugin.getTwofa();
String name = p.getName();
if (twofa == null || !twofa.isDualVerify(p)) return false;
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批#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;
}
/**
* ★第54批:密码校验失败时先同步账号数据(拉回PHP侧凭证入库)再自动重试一次。
* 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风控双验证第二步通过后。
*/
public void finishPasswordLogin(Player p, boolean tempPassword) {
String name = p.getName();
// ★ 登录成功,重置风控
plugin.getRiskControl().onLoginSuccess(name);
plugin.getLoggedIn().add(name);
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
if (plugin.webManager != null) {
plugin.webManager.recordJavaLogin(name, plugin.getPlayerIP(p));
}
plugin.getDb().setLoggedIn(name, true);
if (!tempPassword) {
// 主密码路径保留原有的重复写入(行为与历史版本一致)
plugin.getDb().setLoggedIn(name, true);
}
plugin.getDb().setField(name, "last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name, "last_online_check",
System.currentTimeMillis());
// ★ 立即同步在线状态到PHP(异步,避免阻塞主线程)
if (!tempPassword && plugin.webManager != null) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
plugin.webManager.syncOnlinePlayers();
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage());
}
});
}
// 登录成功关闭飞行
p.setAllowFlight(false);
p.setFlying(false);
// 只在有有效备份时恢复
// ★ 二十二批:DB 读挪异步线程,菜单雪球延后到恢复完成回调
plugin.restoreInventory(p,
() -> plugin.giveMenuSnowball(p));
plugin.recordIPLogin(p);
if (tempPassword) {
// ★ 确保这行存在
plugin.getNeedsPasswordChange().add(name);
}
p.sendMessage(plugin.getConfig2().msg("login_success"));
if (tempPassword) {
p.sendMessage("§c§l[警告] §f您使用的是临时密码,请尽快修改密码!");
p.sendMessage("§7用法: /sdf1_login pw");
}
activateCY(p, name);
// ★ 登录后恢复区域效果
Sdf1_login.AreaProtection areaProt =
plugin.getAreaProtection();
if (areaProt != null) {
areaProt.onPlayerJoin(p);
}
}
public void handleReset(Player p) {
String name = p.getName();
String emailAddr = (String)
plugin.getDb().getField(
name, "email");
if (emailAddr == null
|| emailAddr.isEmpty()) {
p.sendMessage(
"§c未设置邮箱,无法重置密码");
return;
}
String tempPwd = PasswordUtils
.generateTempPassword();
String salt = (String)
plugin.getDb().getField(
name, "password_salt");
String hash = PasswordUtils.hash(
tempPwd, salt);
plugin.getDb().setField(name,
"temp_password", hash);
plugin.getDb().setField(name,
"temp_pw_expire",
System.currentTimeMillis()
+ 300000L);
plugin.getDb().setField(name,
"temp_pw_used", 0);
p.sendMessage("§e正在发送临时密码...");
final String to = emailAddr;
final String fName = name;
final String pwd = tempPwd;
final Main self = plugin;
Bukkit.getScheduler()
.runTaskAsynchronously(self,
() -> {
final boolean[] sent = {false};
Thread t = new Thread(() -> {
sent[0] = self.getEmail()
.sendTempPassword(
to, fName, pwd);
}, "Sdf1_login-reset");
t.setDaemon(true);
t.start();
try {
t.join(15000);
} catch (
InterruptedException ignored) {
}
if (t.isAlive()) {
t.interrupt();
Bukkit.getScheduler().runTask(
self, () ->
p.sendMessage(
"§c邮件发送超时"));
return;
}
Bukkit.getScheduler().runTask(
self, () -> {
if (sent[0]) {
p.sendMessage(
"§a临时密码已发送到 "
+ to);
} else {
p.sendMessage(
"§c邮件发送失败");
}
});
});
}
private void activateCY(Player p,
String name) {
try {
org.bukkit.plugin.Plugin cy =
Bukkit.getPluginManager()
.getPlugin("CY_beibao");
if (cy != null && cy.isEnabled()) {
cy.getClass().getMethod(
"onSdf1Activation",
String.class,
int.class, int.class)
.invoke(cy, name, 0, 0);
}
} catch (Exception ignored) {
}
}
}