277 lines
9.4 KiB
Plaintext
277 lines
9.4 KiB
Plaintext
package Sdf1_login;
|
|
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.entity.Player;
|
|
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
public class RiskControlManager {
|
|
|
|
private final Main plugin;
|
|
|
|
// 玩家当前是否处于风控状态
|
|
private final Map<String, Boolean> riskActive = new ConcurrentHashMap<>();
|
|
// 密码验证连续错误次数
|
|
private final Map<String, Integer> passwordAttempts = new ConcurrentHashMap<>();
|
|
// 邮箱验证码连续错误次数
|
|
private final Map<String, Integer> emailAttempts = new ConcurrentHashMap<>();
|
|
// 风控触发时间(5分钟过期)
|
|
private final Map<String, Long> riskTriggerTime = new ConcurrentHashMap<>();
|
|
// 临时封禁到期时间
|
|
private final Map<String, Long> tempBanExpiry = new ConcurrentHashMap<>();
|
|
|
|
private static final long RISK_TIMEOUT = 5 * 60 * 1000L; // 5分钟
|
|
private static final int MAX_ATTEMPTS = 3;
|
|
private static final long TEMP_BAN_DURATION = 10 * 60 * 1000L; // 10分钟
|
|
|
|
public RiskControlManager(Main plugin) {
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
/**
|
|
* 检查玩家是否被临时封禁
|
|
*/
|
|
public boolean isTempBanned(String name) {
|
|
Long expiry = tempBanExpiry.get(name);
|
|
if (expiry == null) return false;
|
|
if (System.currentTimeMillis() >= expiry) {
|
|
tempBanExpiry.remove(name);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 获取临时封禁剩余秒数
|
|
*/
|
|
public int getTempBanRemaining(String name) {
|
|
Long expiry = tempBanExpiry.get(name);
|
|
if (expiry == null) return 0;
|
|
return (int) ((expiry - System.currentTimeMillis()) / 1000);
|
|
}
|
|
|
|
/**
|
|
* 检查玩家是否需要风控验证
|
|
* 条件:IP变化 + 有背包数据(老玩家)
|
|
*/
|
|
public boolean shouldTrigger(String name, String currentIP) {
|
|
if (isTempBanned(name)) return false;
|
|
if (riskActive.getOrDefault(name, false)) return false;
|
|
|
|
// 获取上次记录的IP
|
|
String lastIP = (String) plugin.getDb().getField(name, "last_ip");
|
|
if (lastIP == null || lastIP.isEmpty()) return false; // 新玩家不触发
|
|
|
|
// IP相同不触发
|
|
if (lastIP.equals(currentIP)) return false;
|
|
|
|
// 有背包数据的老玩家 + IP变化 → 触发风控
|
|
String invData = (String) plugin.getDb().getField(name, "inventory_data");
|
|
if (invData != null && !invData.isEmpty()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 触发风控:冻结玩家,保存并隐藏背包
|
|
*/
|
|
public void trigger(Player p, String currentIP) {
|
|
String name = p.getName();
|
|
riskActive.put(name, true);
|
|
riskTriggerTime.put(name, System.currentTimeMillis());
|
|
passwordAttempts.put(name, 0);
|
|
emailAttempts.put(name, 0);
|
|
|
|
// 保存背包并冻结
|
|
plugin.hideInventory(p);
|
|
|
|
p.sendMessage("§c§l[风控系统] §f检测到异常登录!");
|
|
p.sendMessage("§e当前IP: §f" + currentIP);
|
|
p.sendMessage("§e请选择验证方式:");
|
|
p.sendMessage("§a1. §f密码验证 §7(输入: 密码 <你的密码>)");
|
|
p.sendMessage("§a2. §f邮箱验证 §7(输入: 邮箱)");
|
|
p.sendMessage("§7风控有效期: 5分钟");
|
|
}
|
|
|
|
/**
|
|
* 处理密码验证
|
|
*/
|
|
public boolean handlePassword(Player p, String input) {
|
|
String name = p.getName();
|
|
if (!riskActive.getOrDefault(name, false)) return false;
|
|
|
|
// 检查过期
|
|
if (isExpired(name)) { expireRisk(name); return false; }
|
|
|
|
int attempts = passwordAttempts.getOrDefault(name, 0) + 1;
|
|
passwordAttempts.put(name, attempts);
|
|
|
|
String salt = (String) plugin.getDb().getField(name, "password_salt");
|
|
if (salt == null) return false;
|
|
String hash = PasswordUtils.hash(input, salt);
|
|
|
|
if (plugin.getDb().checkPassword(name, hash)) {
|
|
passRisk(p);
|
|
return true;
|
|
}
|
|
|
|
if (attempts >= MAX_ATTEMPTS) {
|
|
failRisk(p);
|
|
} else {
|
|
p.sendMessage("§c§l[风控系统] §f密码错误!剩余 " + (MAX_ATTEMPTS - attempts) + " 次机会");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 发送邮箱验证码
|
|
*/
|
|
public void sendEmailCode(Player p) {
|
|
String name = p.getName();
|
|
if (!riskActive.getOrDefault(name, false)) return;
|
|
if (isExpired(name)) { expireRisk(name); return; }
|
|
|
|
String emailAddr = (String) plugin.getDb().getField(name, "email");
|
|
if (emailAddr == null || emailAddr.isEmpty()) {
|
|
p.sendMessage("§c§l[风控系统] §f你未绑定邮箱,无法使用邮箱验证");
|
|
return;
|
|
}
|
|
|
|
if (!plugin.getEmail().isConfigured()) {
|
|
p.sendMessage("§c§l[风控系统] §f邮件服务未配置,请使用密码验证");
|
|
return;
|
|
}
|
|
|
|
String code = PasswordUtils.generateVerifyCode();
|
|
plugin.getChatInput().setVerifyCode(p, code, "risk_control");
|
|
boolean sent = plugin.getEmail().sendVerifyCode(emailAddr, name, code);
|
|
if (sent) {
|
|
p.sendMessage("§a§l[风控系统] §f验证码已发送到 " + emailAddr);
|
|
p.sendMessage("§e请输入: 验证 <验证码>");
|
|
} else {
|
|
p.sendMessage("§c§l[风控系统] §f验证码发送失败,请使用密码验证");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 处理邮箱验证码
|
|
*/
|
|
public boolean handleEmailCode(Player p, String code) {
|
|
String name = p.getName();
|
|
if (!riskActive.getOrDefault(name, false)) return false;
|
|
if (isExpired(name)) { expireRisk(name); return false; }
|
|
|
|
int attempts = emailAttempts.getOrDefault(name, 0) + 1;
|
|
emailAttempts.put(name, attempts(p, code, "risk_control")) {
|
|
passRisk(p);
|
|
return true;
|
|
}
|
|
|
|
if (attempts >= MAX_ATTEMPTS) {
|
|
failRisk(p);
|
|
} else {
|
|
p.sendMessage("§c§l[风控系统] §f验证码);
|
|
|
|
if (plugin.getChatInput().checkVerifyCode错误!剩余 " + (MAX_ATTEMPTS - attempts) + " 次机会");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 验证通过:解冻,记录新IP
|
|
*/
|
|
private void passRisk(Player p) {
|
|
String name = p.getName();
|
|
riskActive.remove(name);
|
|
passwordAttempts.remove(name);
|
|
emailAttempts.remove(name);
|
|
riskTriggerTime.remove(name);
|
|
|
|
// 记录新IP
|
|
String ip = plugin.getPlayerIP(p);
|
|
if (ip != null) plugin.getDb().setField(name, "last_ip", ip);
|
|
plugin.recordIPLogin(p);
|
|
|
|
// 还原背包
|
|
plugin.restoreInventory(p);
|
|
plugin.getLoggedIn().add(name);
|
|
plugin.getDb().setLoggedIn(name, true);
|
|
|
|
p.sendMessage("§a§l[风控系统] §f验证通过!已解除风控限制");
|
|
p.playSound(p.getLocation(), org.bukkit.Sound.BLOCK_NOTE_BLOCK_CHIME, 1.0f, 0.79f);
|
|
}
|
|
|
|
/**
|
|
* 验证失败3次:临时封禁 + 邮件报警
|
|
*/
|
|
private void failRisk(Player p) {
|
|
String name = p.getName();
|
|
riskActive.remove(name);
|
|
passwordAttempts.remove(name);
|
|
emailAttempts.remove(name);
|
|
riskTriggerTime.remove(name);
|
|
|
|
// 临时封禁10分钟
|
|
tempBanExpiry.put(name, System.currentTimeMillis() + TEMP_BAN_DURATION);
|
|
|
|
p.sendMessage("§c§l[风控系统] §f验证失败次数过多,临时封禁10分钟");
|
|
|
|
// 邮件报警
|
|
String emailAddr = (String) plugin.getDb().getField(name, "email");
|
|
if (emailAddr != null && !emailAddr.isEmpty() && plugin.getEmail().isConfigured()) {
|
|
String ip = plugin.getPlayerIP(p);
|
|
plugin.getEmail().sendVerifyCode(emailAddr, name,
|
|
"[Sdf1_login] 风控报警: 玩家 " + name + " IP(" + ip + ")验证失败3次,已临时封禁10分钟");
|
|
}
|
|
|
|
// 踢出
|
|
p.kickPlayer("§c§l[风控系统] §f验证失败,临时封禁10分钟,请稍后再试");
|
|
}
|
|
|
|
/**
|
|
* 检查风控是否过期
|
|
*/
|
|
private boolean isExpired(String name) {
|
|
Long trigger = riskTriggerTime.get(name);
|
|
if (trigger == null) return true;
|
|
return System.currentTimeMillis() - trigger > RISK_TIMEOUT;
|
|
}
|
|
|
|
/**
|
|
* 风控过期处理
|
|
*/
|
|
private void expireRisk(String name) {
|
|
riskActive.remove(name);
|
|
passwordAttempts.remove(name);
|
|
emailAttempts.remove(name);
|
|
riskTriggerTime.remove(name);
|
|
}
|
|
|
|
/**
|
|
* 玩家是否处于风控状态
|
|
*/
|
|
public boolean isRiskActive(String name) {
|
|
return riskActive.getOrDefault(name, false);
|
|
}
|
|
|
|
/**
|
|
* 定时清理过期数据
|
|
*/
|
|
public void cleanup() {
|
|
long now = System.currentTimeMillis();
|
|
riskTriggerTime.entrySet().removeIf(e -> {
|
|
if (now - e.getValue() > RISK_TIMEOUT) {
|
|
riskActive.remove(e.getKey());
|
|
passwordAttempts.remove(e.getKey());
|
|
emailAttempts.remove(e.getKey());
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
tempBanExpiry.entrySet().removeIf(e -> now >= e.getValue());
|
|
}
|
|
}
|