opwl接管原生OP名单:加载时导入原生OP,非法持有一律撤销+报警(新增非法持有报警信息模板,支持user/time变量中英文),改为单文件opwl.json
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m0s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

This commit is contained in:
youpaishidifu
2026-10-07 23:05:29 +08:00
parent d19ecc5ce0
commit a39e26caf7
3 changed files with 314 additions and 198 deletions
@@ -9,7 +9,7 @@
},
"我们做了什么": {
"第一层": "临时授权,允许你给OP设置到期时间,到期自动撤销",
"第二层": "防止你的OP乱给别人OP引起人人都是OP"
"第二层": "接管原生OP名单,不在白名单的一律撤销+报警"
},
"你需要做什么": [
"共3步",
@@ -21,6 +21,7 @@
"启用状态": true,
"无固定期限授权列表": [],
"有固定期限授权列表": {},
"非法持有报警信息": "§8[§cOP白名单§8] §f{user} §7不持有授权却持有 OP,已于 §f{time} §7自动撤销 §8| §8[OPWL] §f{user} §7held OP illegally, revoked at §f{time}",
"默认时长": "24h",
"时长支持格式": [
"可以是中文口语:一个小时",
@@ -28,6 +29,17 @@
"可以是标准写法:24H",
"格式很宽松,只要是标准json字符串格式就行。至于你怎么写,插件会解析"
],
"报警模板变量": {
"{user}": "非法持有的玩家名(也认 player / 用户 / 玩家 / username)",
"{time}": "发现时间(也认 时间 / date / 日期)",
"写法": "大小写不敏感,中英文变量名都支持;& 颜色码可用,渲染后自动转成§"
},
"接管说明": [
"本插件【接管原生OP名单】:插件首次加载时,会把当前所有原生OP自动导入「无固定期限授权列表」并写回本文件。",
"之后:任何持有OP但不在白名单的玩家(不在永久表/ 临时已过期 / 时长无法解析),一律自动撤销OP并按「非法持有报警信息」报警。",
"所以以后新增OP请用 /opwl add <玩家>,别用原版 /op;否则会被15秒内的巡检撤掉。",
"白名单全部写在本文件里(单文件),没有子目录也没有附属名单文件。"
],
"以下是模板文件": {
"# 启用状态": true,
"# 无固定期限授权列表": [
@@ -39,6 +51,7 @@
"user2": "1小时",
"user3": "two hour"
},
"# 非法持有报警信息": "支持 {user} 与 {time} 变量,中英文写法都认;留空则用内置默认文案",
"# 默认时长": "24小时"
},
"为什么这次选择json": {
@@ -55,13 +68,12 @@
"opwl removetime user 1h": "给user用户扣1小时授权",
"opwl remove user": "移除user的授权",
"opwl list user": "获取user的授权状态",
"opwl list":"列出OP白名单",
"opwl reload":"重载OP白名单",
"opwl list": "列出OP白名单",
"opwl reload": "重载OP白名单",
"温馨提示": "opwl命令仅限控台执行,普通用户无权",
"加授权完全无感": "因为用户本身没有op时,被授权后,插件立即给予OP并记录授权状态。删除也是。不需要先添加用户在添加授权。"
},
"安全升级": [
"不追求一定是config.json,你可以把文件改成任意.json,这样你可以在目录放一些其它文件,提升伪装。",
"此文件即是配置文件也是说明书。这就意味着,你可以把其它混淆内容一起打包进本json文件,只要保留配置项。其它的你随便删改"
"本文件即是配置文件也是说明书。这就意味着,你可以把其它混淆内容一起打包进本json文件,只要保留配置项。其它的你随便删改"
]
}
+218 -193
View File
@@ -6,7 +6,9 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -21,41 +23,45 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
/**
* OP 白名单(2026-10-07)
*
* 配置目录:plugins/Sdf1_login/OP白名单/ —— 里面【任意一个 .json 文件】就是白名单
* 配置(文件名随意,按文件名排序只读首个;模板 opwl.json 随 jar 释放)。
* 该 json 同时是配置文件与说明书,除四个真实配置项外的其它字段一律忽略、写回时原样保留。
* ★ 本插件【接管原生 OP 名单】:白名单文件是 OP 的唯一真相,原版 ops.json 只剩"存档"。
* - 加载时:把当前所有原生 OP 自动导入「无固定期限授权列表」并写回文件(一次性接管);
* - 运行时:持 OP 但不在白名单(不在永久表 / 临时已过期 / 时长解析失败)=「非法持有」
* → 一律撤销 OP + 按「非法持有报警信息」模板报警(控制台 + 全体在线 OP);
* 报警带 {user}/{time} 变量(支持 user/player/用户/玩家、time/时间,中英文写法都认)。
* - 白名单内的在线玩家缺 OP 时自动补授(原版 /op 给的人若不在白名单,15 秒内会被撤)。
*
* 行为(与 opwl.json 说明书一致):
* - 读得出规则 → 白名单内玩家授 OP;【只授权不主动撤】:白名单外的玩家一律不碰
* (原生 OP / 服主不受影响);本插件授过 OP 的玩家记入 granted(持久化到
* .granted.txt),到期时自动撤销(说明书「到期自动撤销」)。
* - 读不出规则(无 json / JSON 语法错 / 必需字段类型错)→ 【一次性】卸载全部
* 已知玩家(含离线)的 OP,只执行一次,修复配置后自动恢复;
* - 启用状态=false → 功能休眠,不授权也不卸载;
* - 15 秒一次增量热重载:文件指纹(路径+lastModified+长度)有变化才重新解析并打
* 日志,没变化只做静默的授权应用(新上线玩家 / 到期撤销),保持安静;
* - opwl 命令仅限控台(Main 侧对玩家直接静默 return,连帮助都不给)。
* 配置:单文件 plugins/Sdf1_login/opwl.json(模板随 jar 释放,不再有子目录/附属文件)。
* 该 json 同时是配置文件与说明书,除配置项外的其它字段一律忽略、写回时原样保留。
*
* 授权应用只在主线程跑(scheduler 主线程 + 命令 + join 事件),无并发问题。
* 其他行为:
* - 读不出规则(无 json / JSON 语法错 / 必需字段类型错)→ 一次性卸载全部已知 OP,
* 持续失败不重复执行,修复后自动恢复;
* - 启用状态=false → 功能休眠,不授权也不撤销;
* - 15 秒一次增量热重载:文件指纹(路径+lastModified+长度)变化才重解析并打日志;
* - opwl 命令仅限控台(Main 侧对玩家静默 return,连帮助都不给)。
*
* 所有授权/撤销都在主线程执行(scheduler + 命令 + join 事件),无并发问题。
*/
public class OpWhiteListManager implements Listener {
/** 报警节流:同一人 60 秒内不重复报警(防刷屏,但仍会撤销) */
private static final long WARN_COOLDOWN_MS = 60_000L;
private final Main plugin;
private final File dir;
private final File grantedFile;
private final File file;
private final Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
/** 当前被读的 json(pickJson 的结果,命令写回也写它) */
/** 当前白名单文件(= dataFolder/opwl.json) */
private File watched;
/** 解析成功后的完整配置(null = 解析失败态) */
private JsonObject cfg;
@@ -64,57 +70,52 @@ public class OpWhiteListManager implements Listener {
/** 文件指纹(路径 + lastModified + 长度),变化才重载打日志 */
private String lastPath = "";
private long lastStamp = Long.MIN_VALUE;
/** 本插件授过 OP 的玩家(小写名)——兑现「到期自动撤销」的凭据,持久化 */
private final Set<String> granted = new HashSet<>();
/** 报警节流表:小写用户名 → 上次报警毫秒 */
private final Map<String, Long> lastWarn = new HashMap<>();
public OpWhiteListManager(Main plugin) {
this.plugin = plugin;
this.dir = new File(plugin.getDataFolder(), "OP白名单");
this.grantedFile = new File(dir, ".granted.txt");
if (!dir.exists()) {
dir.mkdirs();
this.file = new File(plugin.getDataFolder(), "opwl.json");
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
// 模板随 jar 释放(只在不存在时复制,绝不覆盖用户改过的文件)
File tpl = new File(dir, "opwl.json");
if (!tpl.exists()) {
try (InputStream in = plugin.getResource("OP白名单/opwl.json")) {
if (!file.exists()) {
try (InputStream in = plugin.getResource("opwl.json")) {
if (in != null) {
Files.copy(in, tpl.toPath());
plugin.getLogger().info("[OP白名单] 已释放模板配置: OP白名单/" + tpl.getName());
Files.copy(in, file.toPath());
plugin.getLogger().info("[OP白名单] 已释放配置模板: plugins/Sdf1_login/opwl.json");
} else {
plugin.getLogger().warning("[OPWl白名单] jar 内未找到模板 OP白名单/opwl.json,请手工在目录里放一个 .json");
plugin.getLogger().warning("[OP白名单] jar 内未找到模板 opwl.json,请手工放置该文件");
}
} catch (Exception e) {
plugin.getLogger().warning("[OP白名单] 模板释放失败: " + e.getMessage());
}
}
loadGranted();
reloadInternal(true);
// 15 秒增量热重载 + 静默授权应用
// 接管原生 OP 名单:首次加载先把现有 OP 全导入白名单(一次性)
reloadInternal(true, true);
if (cfg != null) {
importNativeOps();
}
// 15 秒增量热重载 + 静默应用
Bukkit.getScheduler().runTaskTimer(plugin, this::tick, 300L, 300L);
}
// ================= 配置读取 =================
/** 目录里任意一个 .json = 白名单文件;文件名按字典序取首个(确定性) */
private File pickJson() {
File[] fs = dir.listFiles((d, n) -> n.toLowerCase(Locale.ROOT).endsWith(".json"));
if (fs == null || fs.length == 0) {
return null;
}
Arrays.sort(fs, (a, b) -> a.getName().compareToIgnoreCase(b.getName()));
return fs[0];
}
private void reloadInternal(boolean log) {
watched = pickJson();
loadFrom(watched, log);
private void reloadInternal(boolean log, boolean importNative) {
watched = file;
loadFrom(file, log);
updateStamp();
if (importNative && cfg != null) {
importNativeOps();
}
}
private void loadFrom(File f, boolean log) {
if (f == null) {
fail("OP白名单目录里没有任何 .json 文件");
if (f == null || !f.exists()) {
fail("OP白名单配置文件不存在(plugins/Sdf1_login/opwl.json)");
return;
}
try {
@@ -134,19 +135,46 @@ public class OpWhiteListManager implements Listener {
}
cfg = o;
if (parseFailed) {
plugin.getLogger().info("[OP白名单] 配置已恢复正常,恢复授权管理");
plugin.getLogger().info("[OP白名单] 配置已恢复正常,恢复接管");
}
parseFailed = false;
if (log) {
plugin.getLogger().info("[OP白名单] 已重载 " + f.getName() + ":"
plugin.getLogger().info("[OP白名单] 已重载 opwl.json:"
+ (isEnabled() ? "启用" : "停用") + ",无固定期限 "
+ permanentList().size() + " 人,有固定期限 " + timedObject().size() + " 人");
}
} catch (Exception e) {
fail(f.getName() + " 解析失败:" + e.getMessage());
fail("opwl.json 解析失败:" + e.getMessage());
}
}
/**
* ★ 接管原生 OP:把当前所有 OP 且不在白名单的玩家,自动导入「无固定期限授权列表」。
* 幂等——已在白名单的不动;已撤销的非法持有者不再是 OP,下次接管不会把他加回来。
*/
private void importNativeOps() {
List<String> add = new ArrayList<>();
for (OfflinePlayer op : new ArrayList<>(Bukkit.getOperators())) {
String n = op.getName();
if (n == null || n.isEmpty() || !op.isOp()) {
continue;
}
if (hasPermanent(n) || findTimed(n) != null) {
continue;
}
add.add(n);
}
if (add.isEmpty()) {
return;
}
for (String n : add) {
permanentList().add(new JsonPrimitive(n));
}
saveConfig();
plugin.getLogger().info("[OP白名单] 已接管原生 OP 名单:导入 " + add.size()
+ " 人为无固定期限授权 → " + String.join(", ", add));
}
/** 解析失败 → 一次性卸载全部已知 OP(含离线),持续失败不重复执行 */
private void fail(String why) {
cfg = null;
@@ -155,95 +183,122 @@ public class OpWhiteListManager implements Listener {
}
parseFailed = true;
int n = 0;
for (OfflinePlayer op : Bukkit.getOperators()) {
for (OfflinePlayer op : new ArrayList<>(Bukkit.getOperators())) {
if (op.isOp()) {
op.setOp(false);
n++;
}
}
granted.clear();
saveGranted();
lastWarn.clear();
plugin.getLogger().warning("[OP白名单] " + why
+ " —— 已一次性卸载全部 " + n + " 名玩家的 OP;修复 json 后自动恢复授权。");
+ " —— 已一次性卸载全部 " + n + " 名玩家的 OP;修复 json 后自动恢复接管。");
}
/** 文件指纹:路径 + lastModified + 长度(任一变化都视为变更) */
private void updateStamp() {
File f = (watched != null && watched.exists()) ? watched : pickJson();
if (f == null) {
lastPath = "";
lastStamp = 0L;
return;
}
lastPath = f.getAbsolutePath();
lastStamp = f.lastModified() + f.length();
lastPath = file.getAbsolutePath();
lastStamp = file.exists() ? file.lastModified() + file.length() : 0L;
}
// ================= 15 秒 tick =================
private void tick() {
File f = pickJson();
String p = f == null ? "" : f.getAbsolutePath();
long st = f == null ? 0L : f.lastModified() + f.length();
if (!p.equals(lastPath) || st != lastStamp) {
// 有变化:重新解析 + 打日志(含解析失败的告警)
watched = f;
loadFrom(f, true);
long st = file.exists() ? file.lastModified() + file.length() : 0L;
if (!lastPath.equals(file.getAbsolutePath()) || st != lastStamp) {
loadFrom(file, true); // 有变化:重新解析 + 打日志(含解析失败告警)
updateStamp();
}
// 无论有无变化都做一次静默的授权应用(不打无变化日志,保持安静)
// 无论有无变化都做一次静默应用(撤销非法持有 + 补授权),保持安静
apply();
}
/** 玩家上线:立即应用一次(白名单内马上拿到 OP,不用等下个 15 秒) */
/** 玩家上线:立即应用一次(白名单内马上拿到 OP,不等下个 15 秒) */
@EventHandler
public void onJoin(PlayerJoinEvent e) {
apply();
}
// ================= 授权应用(只授不撤,例外仅两处:到期、解析失败) =================
// ================= 接管:撤销非法持有 + 补授权 =================
private void apply() {
if (cfg == null || !isEnabled()) {
return;
}
long now = System.currentTimeMillis();
// 1) 到期自动撤销:只动本插件授过 OP 且当前在「有固定期限」表里、已过期的玩家。
// 白名单外的其他人(原生 OP 等)一律不碰。
boolean dirty = false;
Iterator<String> it = granted.iterator();
while (it.hasNext()) {
String key = it.next();
String timed = findTimed(key);
if (timed == null) {
// 1) 非法持有扫描:持 OP 但不在白名单 → 撤销 + 报警
for (OfflinePlayer op : new ArrayList<>(Bukkit.getOperators())) {
String n = op.getName();
if (n == null || n.isEmpty() || !op.isOp()) {
continue;
}
long exp = expireOf(timed);
if (exp > 0 && exp <= now) {
setOpByName(key, false);
it.remove();
dirty = true;
plugin.getLogger().info("[OP白名单] " + key + " 授权到期,已自动撤销 OP");
if (isWhitelisted(n)) {
continue;
}
setOpByName(n, false);
warnIllegal(n, now, describeIllegal(n));
}
if (dirty) {
saveGranted();
}
// 2) 白名单内在线玩家授权(已是 OP 的跳过;新授的记入 granted)
// 2) 白名单内在线玩家补授(已是 OP 的跳过)
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.isOp()) {
continue;
}
if (!isWhitelisted(p.getName())) {
if (p.isOp() || !isWhitelisted(p.getName())) {
continue;
}
p.setOp(true);
granted.add(p.getName().toLowerCase(Locale.ROOT));
saveGranted();
plugin.getLogger().info("[OP白名单] 已授权 " + p.getName() + " 获得 OP");
}
}
/** 撤销非法持有 + 报警(控制台 + 全体在线 OP),带 60 秒节流 */
private void warnIllegal(String name, long now, String reason) {
String key = name.toLowerCase(Locale.ROOT);
long last = lastWarn.getOrDefault(key, 0L);
if (last > 0 && now - last < WARN_COOLDOWN_MS) {
return;
}
lastWarn.put(key, now);
String alarm = renderAlarm(name, now);
plugin.getLogger().warning("[OP白名单] 非法持有已撤销 | 原因: " + reason + " | " + stripColor(alarm));
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.isOp()) {
p.sendMessage(alarm);
}
}
}
/** 报警模板渲染:{user}/{player}/{用户}/{玩家} 与 {time}/{时间} 变量(大小写不敏感) */
private String renderAlarm(String user, long now) {
String tpl = alarmTemplate();
String time = fmtFull(now);
String s = tpl;
for (String k : new String[]{"user", "player", "用户", "玩家", "username"}) {
s = s.replaceAll("(?i)\\{" + k + "\\}", Matcher.quoteReplacement(user));
}
for (String k : new String[]{"time", "时间", "date", "日期"}) {
s = s.replaceAll("(?i)\\{" + k + "\\}", Matcher.quoteReplacement(time));
}
return ChatColor.translateAlternateColorCodes('&', s);
}
/** 报警模板:配置项「非法持有报警信息」,空则用内置默认(中英双语) */
private String alarmTemplate() {
if (cfg != null && cfg.has("非法持有报警信息")
&& cfg.get("非法持有报警信息").isJsonPrimitive()) {
String s = cfg.get("非法持有报警信息").getAsString().trim();
if (!s.isEmpty()) {
return s;
}
}
return "§8[§cOP白名单§8] §f{user} §7不持有授权却持有 OP,已于 §f{time} §7自动撤销"
+ " §8| §8[OPWL] §f{user} §7held OP illegally, revoked at §f{time}";
}
/** 撤销原因(仅日志用,便于排查是哪一类"不在白名单") */
private String describeIllegal(String name) {
if (findTimed(name) != null) {
return "临时授权已过期或时长无法解析(" + findTimed(name) + ")";
}
return "不在白名单(" + (Bukkit.getPlayerExact(name) != null ? "在线" : "离线") + ")";
}
private void setOpByName(String name, boolean op) {
Player p = Bukkit.getPlayerExact(name);
if (p != null) {
@@ -310,10 +365,6 @@ public class OpWhiteListManager implements Listener {
}
private boolean removeTimed(String name) {
String k = timedKeyOf(name);
if (timedObject().has(k) && !k.equalsIgnoreCase(name) && timedObject().get(k) != null) {
// key 原样存在
}
for (String key : new ArrayList<>(timedObject().keySet())) {
if (name.equalsIgnoreCase(key)) {
timedObject().remove(key);
@@ -323,7 +374,7 @@ public class OpWhiteListManager implements Listener {
return false;
}
/** 是否白名单有效成员:永久 或 有期限且未过期(坏时长视为无效,不授) */
/** 是否白名单有效成员:永久 或 有期限且未过期(坏时长视为无效 → 算非法持有) */
private boolean isWhitelisted(String name) {
if (cfg == null) {
return false;
@@ -339,7 +390,7 @@ public class OpWhiteListManager implements Listener {
return e > 0 && e > System.currentTimeMillis();
}
/** 解析为绝对到期毫秒;解析失败返回 -1(调用方按「无效/不过期」保守处理) */
/** 解析为绝对到期毫秒;解析失败返回 -1(调用方按「无效」保守处理) */
private long expireOf(String v) {
try {
return DurationParser.parseToExpireMs(v);
@@ -360,50 +411,20 @@ public class OpWhiteListManager implements Listener {
// ================= 持久化 =================
/** 写回 json:保留用户在配置项之外塞的所有字段(说明书/混淆内容),只动配置项 */
/** 写回 json:保留配置项之外的所有说明书/混淆字段,只动配置项 */
private void saveConfig() {
try {
File target = watched;
if (target == null || !target.exists()) {
target = new File(dir, "opwl.json");
watched = target;
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
Files.write(target.toPath(), gson.toJson(cfg).getBytes(StandardCharsets.UTF_8));
Files.write(file.toPath(), gson.toJson(cfg).getBytes(StandardCharsets.UTF_8));
updateStamp(); // 命令自己有回馈,抑制下个 tick 再打一次「已重载」
} catch (Exception e) {
plugin.getLogger().warning("[OP白名单] 配置写回失败: " + e.getMessage());
}
}
private void loadGranted() {
granted.clear();
if (!grantedFile.exists()) {
return;
}
try {
for (String line : Files.readAllLines(grantedFile.toPath(), StandardCharsets.UTF_8)) {
String s = line.trim().toLowerCase(Locale.ROOT);
if (!s.isEmpty()) {
granted.add(s);
}
}
} catch (Exception e) {
plugin.getLogger().warning("[OP白名单] 授权痕迹读取失败: " + e.getMessage());
}
}
private void saveGranted() {
try {
StringBuilder sb = new StringBuilder();
for (String s : granted) {
sb.append(s).append('\n');
}
Files.write(grantedFile.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
plugin.getLogger().warning("[OP白名单] 授权痕迹写入失败: " + e.getMessage());
}
}
// ================= opwl 命令(仅控台;Main 已拦玩家) =================
public void onCommand(org.bukkit.command.CommandSender sender, String[] args) {
@@ -414,14 +435,14 @@ public class OpWhiteListManager implements Listener {
sendHelp(sender);
return;
case "reload": {
reloadInternal(true);
reloadInternal(true, false);
apply();
sender.sendMessage(cfg == null
? "§c重载完成,但配置解析失败(详见控制台告警)"
: "§a重载完成:§f" + (isEnabled() ? "启用" : "停用")
+ ",无固定期限 " + permanentList().size()
+ " 人,有固定期限 " + timedObject().size() + " 人" +
"§d欢迎来到草原探险服务器,ip: mc2.ypshidifu.cn 端口(基岩版需要):30679");
+ " 人,有固定期限 " + timedObject().size() + " 人");
sender.sendMessage("§7提示:原生 OP 只在插件【首次加载】时导入;之后请用 /opwl add 或改 json 加人");
return;
}
default:
@@ -429,7 +450,7 @@ public class OpWhiteListManager implements Listener {
}
// 以下子命令需要配置可读
if (cfg == null) {
sender.sendMessage("§c配置解析失败,先修复 OP白名单 目录里的 json 再操作(或 /opwl reload)");
sender.sendMessage("§c配置解析失败,先修复 plugins/Sdf1_login/opwl.json 再操作(或 /opwl reload)");
return;
}
boolean readOnly = "list".equals(sub);
@@ -444,12 +465,13 @@ public class OpWhiteListManager implements Listener {
return;
}
String name = args[1].trim();
boolean moved = removeTimed(name); // 已有临时授权 → 转永久
boolean moved = removeTimed(name);
if (!hasPermanent(name)) {
permanentList().add(new com.google.gson.JsonPrimitive(name));
permanentList().add(new JsonPrimitive(name));
}
saveConfig();
boolean ok = grantNow(name, true);
lastWarn.remove(name.toLowerCase(Locale.ROOT));
boolean ok = setOpNow(name);
sender.sendMessage("§a已添加 §f" + name + " §a为无固定期限授权"
+ (moved ? "(已把原临时授权转为永久)" : "")
+ (ok ? ",OP 已授予" : "(玩家不在本机档案中,上线时自动授予)"));
@@ -474,10 +496,11 @@ public class OpWhiteListManager implements Listener {
sender.sendMessage("§c该时长的到期时间已在过去: " + dur);
return;
}
removePermanent(name); // 临时授权优先:从永久表移出
removePermanent(name);
timedObject().addProperty(timedKeyOf(name), dur);
saveConfig();
boolean ok = grantNow(name, true);
lastWarn.remove(name.toLowerCase(Locale.ROOT));
boolean ok = setOpNow(name);
sender.sendMessage("§a已给 §f" + name + " §a添加临时授权 §7(时长 " + dur
+ ",到期 " + fmt(exp) + ")" + (ok ? ",OP 已授予" : ",上线时自动授予"));
return;
@@ -519,9 +542,8 @@ public class OpWhiteListManager implements Listener {
if (newExp <= System.currentTimeMillis()) {
removeTimed(name);
saveConfig();
boolean revoked = revokeNow(name);
sender.sendMessage("§a扣减后 " + name + " 的授权已到期,条目已移除"
+ (revoked ? ",OP 已撤销" : ""));
setOpByName(name, false);
sender.sendMessage("§a扣减后 " + name + " 的授权已到期,条目已移除,OP 已撤销");
} else {
String s = fmt(newExp);
timedObject().addProperty(timedKeyOf(name), s);
@@ -540,13 +562,15 @@ public class OpWhiteListManager implements Listener {
boolean inPerm = removePermanent(name);
boolean inTimed = removeTimed(name);
saveConfig();
boolean revoked = revokeNow(name);
if (!inPerm && !inTimed && !revoked) {
sender.sendMessage("§e" + name + " 本就不在白名单中");
boolean wasOp = isOpNow(name);
setOpByName(name, false);
lastWarn.remove(name.toLowerCase(Locale.ROOT));
if (!inPerm && !inTimed && !wasOp) {
sender.sendMessage("§e" + name + " 本就不在白名单中,也没有 OP");
} else {
sender.sendMessage("§a已移除 §f" + name + " §a的授权"
+ (revoked ? ",OP 已撤销"
: ((inPerm || inTimed) ? "(该玩家 OP 非本插件授予,OP 状态未动)" : "")));
+ (wasOp ? ",OP 已撤销" : ""));
sender.sendMessage("§7提示:若要长用请 /opwl add " + name + ",否则他的 OP 会被自动接管撤销");
}
return;
}
@@ -555,8 +579,9 @@ public class OpWhiteListManager implements Listener {
listOne(sender, args[1].trim());
return;
}
sender.sendMessage("§e===== OP白名单 §7(" + (watched != null ? watched.getName() : "?") + ") =====");
sender.sendMessage("§7状态: " + (isEnabled() ? "§a启用" : "§c停用")
sender.sendMessage("§e===== OP白名单(接管原生 OP) =====");
sender.sendMessage("§7配置: §fplugins/Sdf1_login/opwl.json §7| 状态: "
+ (isEnabled() ? "§a启用" : "§c停用")
+ " §7| 解析: " + (parseFailed ? "§c失败" : "§a正常"));
JsonArray pu = permanentList();
StringBuilder sb = new StringBuilder("§e无固定期限(" + pu.size() + "): ");
@@ -580,52 +605,46 @@ public class OpWhiteListManager implements Listener {
long e = expireOf(v);
String state = e <= 0 ? "§c到期时间无法解析: " + v
: (e > now ? "§7到期 §f" + fmt(e) + " §7(剩 " + human(e - now) + ")"
: "§c已过期 " + fmt(e));
: "§c已过期 " + fmt(e) + "(OP 将被撤销)");
sender.sendMessage("§7 " + en.getKey() + " → " + state);
}
// 额外:谁在偷偷持有 OP(白名单外的)
List<String> illegal = new ArrayList<>();
for (OfflinePlayer op : new ArrayList<>(Bukkit.getOperators())) {
String n = op.getName();
if (n != null && !n.isEmpty() && op.isOp() && !isWhitelisted(n)) {
illegal.add(n);
}
}
sender.sendMessage("§e当前持 OP 但不在白名单(" + illegal.size() + "): "
+ (illegal.isEmpty() ? "§a无" : "§c" + String.join("§7, §c", illegal)));
return;
}
default:
sendHelp(sender);
return;
}
}
// ================= 命令辅助 =================
/** 立即授予/撤销并记录;返回 false = 档案未知(上线时由 join 自动补授) */
private boolean grantNow(String name, boolean op) {
if (!op) {
return revokeNow(name);
}
/** 立即授 OP;返回 false = 档案未知(上线时由 join 自动补授) */
private boolean setOpNow(String name) {
Player p = Bukkit.getPlayerExact(name);
if (p != null) {
p.setOp(true);
granted.add(name.toLowerCase(Locale.ROOT));
saveGranted();
return true;
}
OfflinePlayer o = Bukkit.getOfflinePlayer(name);
if (!o.hasPlayedBefore()) {
// 档案未知:留到 join 兜底(isWhitelisted && !isOp → 授)
return false;
}
o.setOp(true);
granted.add(name.toLowerCase(Locale.ROOT));
saveGranted();
return true;
}
/** 仅撤销本插件授过 OP 的玩家;返回是否真的撤了 */
private boolean revokeNow(String name) {
String key = name.toLowerCase(Locale.ROOT);
if (!granted.contains(key)) {
return false;
}
setOpByName(name, false);
granted.remove(key);
saveGranted();
return true;
private boolean isOpNow(String name) {
Player online = Bukkit.getPlayerExact(name);
return online != null ? online.isOp() : Bukkit.getOfflinePlayer(name).isOp();
}
private void listOne(org.bukkit.command.CommandSender s, String name) {
@@ -638,30 +657,27 @@ public class OpWhiteListManager implements Listener {
long now = System.currentTimeMillis();
if (e > 0) {
s.sendMessage("§7 类型: §f临时 §7到期 §f" + fmt(e)
+ (e > now ? " §7(剩 " + human(e - now) + ")" : " §c(已过期)"));
+ (e > now ? " §7(剩 " + human(e - now) + ")" : " §c(已过期,OP 会被撤销)"));
} else {
s.sendMessage("§7 类型: §f临时 §c(到期时间无法解析: " + t + ")");
s.sendMessage("§7 类型: §f临时 §c(到期时间无法解析: " + t + ",OP 会被撤销)");
}
} else {
s.sendMessage("§7 类型: §c不在白名单");
s.sendMessage("§7 类型: §c不在白名单(持有 OP 会被自动撤销并报警)");
}
Player online = Bukkit.getPlayerExact(name);
boolean isOp = online != null ? online.isOp() : Bukkit.getOfflinePlayer(name).isOp();
s.sendMessage("§7 当前OP: " + (isOp ? "§f是" : "§f否")
+ " §7| 本插件授OP记录: " + (granted.contains(name.toLowerCase(Locale.ROOT)) ? "§f是" : "§f否"));
s.sendMessage("§7 当前OP: " + (isOpNow(name) ? "§f是" : "§f否"));
}
private void sendHelp(org.bukkit.command.CommandSender s) {
s.sendMessage("§e===== OP白名单(仅控制台可用) =====");
s.sendMessage("§e===== OP白名单(仅控制台可用,接管原生 OP) =====");
s.sendMessage("§f/opwl §7- 本帮助");
s.sendMessage("§f/opwl add <玩家> §7- 添加无固定期限授权");
s.sendMessage("§f/opwl addtime <玩家> [时长] §7- 添加临时授权(缺省用默认时长 "
+ defaultDuration() + ")");
s.sendMessage("§f/opwl addtime <玩家> [时长] §7- 添加临时授权(缺省 " + defaultDuration() + ")");
s.sendMessage("§f/opwl removetime <玩家> <扣减时长> §7- 扣减授权时长(如 1h)");
s.sendMessage("§f/opwl remove <玩家> §7- 移除授权");
s.sendMessage("§f/opwl list [玩家] §7- 查看白名单 / 单个玩家状态");
s.sendMessage("§f/opwl remove <玩家> §7- 移除授权并撤销其 OP");
s.sendMessage("§f/opwl list [玩家] §7- 查看白名单 / 非法持有者 / 单人状态");
s.sendMessage("§f/opwl reload §7- 重载配置");
s.sendMessage("§7配置目录: plugins/Sdf1_login/OP白名单/(任意 *.json),15 秒自动热重载");
s.sendMessage("§7配置: plugins/Sdf1_login/opwl.json(单文件,白名单全在里面),15 秒自动热重载");
s.sendMessage("§7报警模板变量: §f{user}§7=玩家名 §f{time}§7=发现时间(中英文写法均可)");
s.sendMessage("§d欢迎来到草原探险服务器,ip: mc2.ypshidifu.cn 端口(基岩版需要):30679");
}
@@ -681,6 +697,10 @@ public class OpWhiteListManager implements Listener {
return new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date(ms));
}
private static String fmtFull(long ms) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(ms));
}
/** 剩余时长人性化:X天X小时X分 */
private static String human(long ms) {
long min = ms / 60000L;
@@ -702,4 +722,9 @@ public class OpWhiteListManager implements Listener {
}
return sb.length() == 0 ? "不到1分钟" : sb.toString();
}
/** 日志里去掉颜色符号,避免控制台刷屏乱码 */
private static String stripColor(String s) {
return s == null ? "" : s.replaceAll("(?i)&[0-9a-k]", "").replaceAll("\u00a7[0-9a-k]", "");
}
}
+79
View File
@@ -0,0 +1,79 @@
{
"OP白名单": "服务器的好管家",
"为什么要OP白名单": {
"根因有3点": {
"第一点": "已有OP乱给权限防不住",
"第二点": "有人利用服务器漏洞拿到OP权限",
"第三点": "不信任OP"
}
},
"我们做了什么": {
"第一层": "临时授权,允许你给OP设置到期时间,到期自动撤销",
"第二层": "接管原生OP名单,不在白名单的一律撤销+报警"
},
"你需要做什么": [
"共3步",
"第一步:安装sdf1_login插件",
"第二步改这个文件",
"第三步按参考格式或后台指令添加授权"
],
"----": "----",
"启用状态": true,
"无固定期限授权列表": [],
"有固定期限授权列表": {},
"非法持有报警信息": "§8[§cOP白名单§8] §f{user} §7不持有授权却持有 OP,已于 §f{time} §7自动撤销 §8| §8[OPWL] §f{user} §7held OP illegally, revoked at §f{time}",
"默认时长": "24h",
"时长支持格式": [
"可以是中文口语:一个小时",
"可以是英文one hour",
"可以是标准写法:24H",
"格式很宽松,只要是标准json字符串格式就行。至于你怎么写,插件会解析"
],
"报警模板变量": {
"{user}": "非法持有的玩家名(也认 player / 用户 / 玩家 / username)",
"{time}": "发现时间(也认 时间 / date / 日期)",
"写法": "大小写不敏感,中英文变量名都支持;& 颜色码可用,渲染后自动转成§"
},
"接管说明": [
"本插件【接管原生OP名单】:插件首次加载时,会把当前所有原生OP自动导入「无固定期限授权列表」并写回本文件。",
"之后:任何持有OP但不在白名单的玩家(不在永久表/ 临时已过期 / 时长无法解析),一律自动撤销OP并按「非法持有报警信息」报警。",
"所以以后新增OP请用 /opwl add <玩家>,别用原版 /op;否则会被15秒内的巡检撤掉。",
"白名单全部写在本文件里(单文件),没有子目录也没有附属名单文件。"
],
"以下是模板文件": {
"# 启用状态": true,
"# 无固定期限授权列表": [
"user1",
"user2"
],
"# 有固定期限授权列表": {
"user1": "2026-10-06 23:00",
"user2": "1小时",
"user3": "two hour"
},
"# 非法持有报警信息": "支持 {user} 与 {time} 变量,中英文写法都认;留空则用内置默认文案",
"# 默认时长": "24小时"
},
"为什么这次选择json": {
"根因依旧2点": {
"1": "防止非专业人士修改本文件",
"2": "使用专业格式,让小白试探着劝退(一旦解析失败,所有玩家OP全部卸载)"
}
},
"指令参数": {
"opwl": "帮助菜单",
"opwl add user": "添加user用户为无固定期限用户",
"opwl addtime user": "按默认时长添加临时授权用户",
"opwl addtime user 1h": "给user用户价1小时授权",
"opwl removetime user 1h": "给user用户扣1小时授权",
"opwl remove user": "移除user的授权",
"opwl list user": "获取user的授权状态",
"opwl list": "列出OP白名单",
"opwl reload": "重载OP白名单",
"温馨提示": "opwl命令仅限控台执行,普通用户无权",
"加授权完全无感": "因为用户本身没有op时,被授权后,插件立即给予OP并记录授权状态。删除也是。不需要先添加用户在添加授权。"
},
"安全升级": [
"本文件即是配置文件也是说明书。这就意味着,你可以把其它混淆内容一起打包进本json文件,只要保留配置项。其它的你随便删改"
]
}