第四章好像比前面几张都快……笔者一拖再拖,觉得这个项目似乎没法再拖下去了,毕竟我们讲完了数据库、邮件发送,貌似是时候完成这个项目了。
你可能已经注意到了,本节的编号不是以 EX 开头,而是 AC。这表示我们将要完成一次真正的行动了。所有 EX 开头的行动,笔者都已经做过实验,是直接给你正确的结果。而对于 AC 章节,笔者也不保证自己找到的就是最好的解决方案,还真有点紧张呢~
行动名称:HarmonyAuth SMART
行动代号:AC-1
行动类别:作战
涉及章节:
- AC-1-1
- AC-1-2
- AC-1-3
难度:闪电苦力怕
这次我们的任务很明确:改进「HarmonyAuth」。
我们大致要做出这么几个功能:
- 使用 OP 审核来恢复密码
- 自由地修改密码
- 使用数据库存储数据,文件作为后备
- 让玩家不再回弹
嗯,大致就是这些。
首先下载(如果你之前没有下载的)mysql-connector-java-8.0.23.jar
,版本无所谓。这一次考虑到邮件不太好用,就没有使用,而是采用了更为直观的管理员手动审核功能。
接下来和 EX-1-1 一样,创建新模块「HarmonyAuth SMART」,添加依赖。
回到代码界面,创建包,创建主类继承 JavaPlugin
,这应该很熟练了。
我们先把配置文件写好,这样在处理数据时就知道该做些什么。
创建 config.yml
:
mysql:
enabled: false
host: localhost
port: 3306
username: root
password: 123456
db-name: "harmony-auth-smart"
msg:
hint-register: "请输入 /has <密码> <再输入一次密码> 进行或注册!"
hint-login: "请输入 /has <密码> 进行登录!"
login-failed: "密码错误!如忘记密码,使用 /iforgot 恢复密码!"
login-success: "登录成功!"
register-success: "注册成功!"
register-failed: "两次输入密码不一致!"
iforgot-newpwd: "请在聊天栏输入新密码。"
iforgot-hint: "请在聊天栏向服服务器管理人员说明情况并提供证据。"
iforgot-commit: "您的请求已经上报。"
iforgot-accepted: "恢复请求已经通过,使用您设置的新密码登录。"
iforgot-rejected: "恢复请求未通过,请尝试使用原先的密码登录或提交新的申请。"
iforgot-no-available: "密码恢复功能已禁用。"
command-handling: "上一条命令正在处理中,请稍后再试!"
audit-in: "您已进入审核模式。"
audit-out: "您已退出审核模式。"
audit-uuid: "玩家 UUID:"
audit-reason: "申请原因:"
audit-hint: "输入 y 通过或输入 n 拒绝,输入 q 离开审核模式。"
hint: "在登录前,您不能进行操作。"
iforgot: true # 允许 IForgot
auto-login: 300 # 自动登录间隔,单位秒
hook: # 用 ${playerName} 表示玩家名
on-login-success: # 登录成功
- ""
on-login-failed: # 登录失败
- ""
on-register-success: # 注册成功
- ""
为了使用文件进行后备存储,这里我们不能粗暴地写入 Bukkit 的默认配置(config.yml
),而应该使用自定义配置文件。
自定义配置文件和默认配置文件一样,都需要在 src
下创建。名字是无所谓的(不然为什么叫自定义呢)。
创建 data.yml
,并留空(本来就是数据文件)。
自定义文件需要自己进行读取和保存,利用 Java 的接口特性,我们编写一个 IDataManager
接口,到时候分别用文件和数据库方式实现它们(自己给自己创建一个协议用)。
package rarityeg.harmonyauthsmart;
import javax.annotation.Nonnull;
import java.util.Date;
import java.util.UUID;
public interface IDataManager {
void saveAll();
void loadAll();
@Nonnull
String getPasswordHash(UUID id);
boolean getIForgotState(UUID id);
@Nonnull
String getIForgotManualReason(UUID id);
@Nonnull
String getIForgotNewPasswordHash(UUID id);
@Nonnull
Date getLastLoginTime(UUID id);
void setPasswordHash(UUID id, String hash);
void setIForgotState(UUID id, boolean state);
void setIForgotManualReason(UUID id, String reason);
void setIForgotNewPasswordHash(UUID id, String hash);
void setLastLoginTime(UUID id, Date date);
boolean isExist(UUID id);
}
接口中只允许抽象方法。
然后我们先实现 IDataManager
的文件版本,在此之前先在主类上完成那个小技巧,将实例暴露出去,另外顺便也把数据库驱动注册上:
package rarityeg.harmonyauthsmart;
import org.bukkit.plugin.java.JavaPlugin;
public class HarmonyAuthSMART extends JavaPlugin {
public static JavaPlugin instance;
public static boolean dbError = false;
@Override
public void onEnable() {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
getLogger().log(Level.WARNING, "数据库驱动加载失败,将使用备用存储方法。");
e.printStackTrace();
dbError = true;
// 如果驱动找不到就改用文件存储
}
saveDefaultConfig(); // config.yml
saveResource("data.yml", false); // data.yml,false 表示不覆盖
instance = this;
}
}
saveResource
用于保存 src
下对应的文件,第二个参数是「文件存在时,是否覆盖」的意思。
接下来实现 FileDataManager
类:
package rarityeg.harmonyauthsmart;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
import java.util.UUID;
import java.util.logging.Level;
public class FileDataManager implements IDataManager {
static FileConfiguration data;
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// Date 没有办法直接序列化,需要利用 DateFormat
@Override
public void saveAll() {
try {
File dataFile = new File(HarmonyAuthSMART.instance.getDataFolder(), "data.yml");
data.save(dataFile);
// 保存数据的标准方法
} catch (IOException e) {
HarmonyAuthSMART.instance.getLogger().log(Level.WARNING, "配置数据未能保存,可能产生回档问题!");
// 这种错误还是说出来的好
e.printStackTrace();
}
}
@Override
public void loadAll() {
File dataFile = new File(HarmonyAuthSMART.instance.getDataFolder(), "data.yml");
data = YamlConfiguration.loadConfiguration(dataFile);
// 不需要 InputStream,直接 loadConfiguration
}
@Override
@Nonnull
public String getPasswordHash(UUID id) {
return Objects.requireNonNull(data.getString("passwords." + id.toString(), ""));
// 虽然 getString 提供了默认值就不会返回 null,但 IDEA 一直报警告很麻烦,就照它的建议做了
}
@Override
public boolean getIForgotState(UUID id) {
return data.getBoolean("iforgot-states." + id.toString());
// boolean 不会返回 null,默认是 false
}
@Override
@Nonnull
public String getIForgotManualReason(UUID id) {
return Objects.requireNonNull(data.getString("iforgot-reasons." + id.toString(), ""));
}
@Override
@Nonnull
public String getIForgotNewPasswordHash(UUID id) {
// IForgot 会先向玩家要求一个新密码,用这个查询
return Objects.requireNonNull(data.getString("iforgot-newpwd." + id.toString(), ""));
}
@Override
@Nonnull
public Date getLastLoginTime(UUID id) {
String dstr = data.getString("last-login." + id.toString(), "1970-01-01 23:59:59");
if (dstr == null) {
// 实际上这里不可能执行到,getString 返回的不可能是 null
try {
return sdf.parse("1970-01-01 23:59:59");
} catch (ParseException e) {
HarmonyAuthSMART.instance.getLogger().log(Level.WARNING, "这不可能!不可能出现这个错误!日期的读取失败了?");
e.printStackTrace();
return new Date();
}
} else {
try {
return sdf.parse(dstr);
} catch (ParseException e) {
// 这里也不可能执行到,以防万一
try {
return sdf.parse("1970-01-01 23:59:59");
} catch (ParseException e2) {
HarmonyAuthSMART.instance.getLogger().log(Level.WARNING, "这不可能!不可能出现这个错误!日期的读取失败了?");
e2.printStackTrace();
return new Date();
}
}
}
}
// 以下都是上面相应的 set 方法
@Override
public void setPasswordHash(UUID id, String hash) {
data.set("passwords." + id.toString(), hash);
}
@Override
public void setIForgotState(UUID id, boolean state) {
data.set("iforgot-states." + id.toString(), state);
}
@Override
public void setIForgotManualReason(UUID id, String reason) {
data.set("iforgot-reasons." + id.toString(), reason);
}
@Override
public void setIForgotNewPasswordHash(UUID id, String hash) {
data.set("iforgot-newpwd." + id.toString(), hash);
}
@Override
public void setLastLoginTime(UUID id, Date date) {
data.set("last-login." + id.toString(), sdf.format(date));
}
@Override
public boolean isExist(UUID id) {
return data.contains("passwords." + id.toString());
}
}
虽然看上去很多,实际上原理很简单啦,有注释应该看得懂。
getDataFolder
用于获取当前插件的数据目录。一般是 <服务器根目录>/plugins/<插件的名字>/
。
下面我们实现基于数据库的 DBDataManager
。这里暂时不用异步(因为不好回调),我们在命令处理器中再使用异步。
在继续编写代码前,我们需要部署好数据库。下载安装好 MySQL 并启动它(如果你没有安装过)。
打开 MySQL 终端(希望你还记得怎么做,见 4-2),然后(在 MySQL 终端中)创建新的数据库用于测试:
CREATE DATABASE test;
名字是无所谓的。
回到 IDEA,在窗口的最右侧找到「Database」,单击「+」、「Data Source」、「MySQL」,打开「Data Sources and Drivers」窗口。
这里 IDEA 会提示你没有安装驱动程序,单击「Download Missing Drivers」即可。
「User」填上用户名(一般是 root
),「Password」输入密码,「Database」填入刚刚创建的数据库名(test
),单击一下「Test Connection」,确认连接成功。
?> 到底怎么回事?
作为一个成熟的开发工具,IDEA 认为我们都是针对特定的数据库进行开发的,因此它需要以一个数据库作为蓝本来进行语法检查。
但这次我们不需要这样的功能,不过报错放在那里总不是个办法,于是就部署一个数据库解决这个问题。
实际上上面这些操作即使现在不做,调试时也要做的,还是早点搞定的好。
然后我们就可以实现 DBDataManager
了。
package rarityeg.harmonyauthsmart;
import org.bukkit.configuration.file.FileConfiguration;
import javax.annotation.Nonnull;
import java.sql.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
import java.util.UUID;
public class DBDataManager implements IDataManager {
static String db_url;
static String username;
static String password;
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void saveAll() {
}
@Override
public void loadAll() {
FileConfiguration fc = HarmonyAuthSMART.instance.getConfig();
String port = fc.getString("mysql.port");
username = fc.getString("mysql.username");
String db_name = fc.getString("mysql.db-name");
password = fc.getString("mysql.password");
if (port == null || username == null || db_name == null || password == null) {
HarmonyAuthSMART.dbError = true;
HarmonyAuthSMART.instance.getLogger().warning("数据库配置不完全,将改用备用存储方式。");
return;
}
db_url = "jdbc:mysql://localhost:" + port + "/" + db_name + "?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC";
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
Statement statement = connection.createStatement();
statement.execute("CREATE TABLE IF NOT EXISTS harmony_auth_data(UUID VARCHAR(255) PRIMARY KEY NOT NULL, PwdHash TEXT NOT NULL, IForgotState BOOLEAN NOT NULL, IForgotReason LONGTEXT NOT NULL, NewPwdHash TEXT NOT NULL, LastLogin TEXT NOT NULL);");
statement.close();
connection.close();
HarmonyAuthSMART.instance.getLogger().info("成功与数据库建立连接!");
// 先尝试一次连接
} catch (SQLException e) {
putError(e);
}
}
private void putError(Exception e) {
HarmonyAuthSMART.dbError = true;
HarmonyAuthSMART.instance.getLogger().warning("数据库操作失败,将改用备用存储方式。");
e.printStackTrace();
}
@Nonnull
@Override
public String getPasswordHash(UUID id) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("SELECT PwdHash FROM harmony_auth_data WHERE UUID=?;");
// 查询
preparedStatement.setString(1, id.toString());
// SQL 的索引从 1 开始!!!
ResultSet rs = preparedStatement.executeQuery();
rs.first();
// 按道理只应该返回一组,多的我们舍去
preparedStatement.close();
connection.close();
return Objects.requireNonNullElse(rs.getString("PwdHash"), "");
} catch (SQLException e) {
putError(e);
return "";
}
}
@Override
public boolean getIForgotState(UUID id) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("SELECT IForgotState FROM harmony_auth_data WHERE UUID=?;");
preparedStatement.setString(1, id.toString());
ResultSet rs = preparedStatement.executeQuery();
rs.first();
preparedStatement.close();
connection.close();
return Objects.requireNonNullElse(rs.getBoolean("IForgotState"), false);
} catch (SQLException e) {
putError(e);
return false;
}
}
@Nonnull
@Override
public String getIForgotManualReason(UUID id) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("SELECT IForgotReason FROM harmony_auth_data WHERE UUID=?;");
preparedStatement.setString(1, id.toString());
ResultSet rs = preparedStatement.executeQuery();
rs.first();
preparedStatement.close();
connection.close();
return Objects.requireNonNullElse(rs.getString("IForgotReason"), "");
} catch (SQLException e) {
putError(e);
return "";
}
}
@Nonnull
@Override
public String getIForgotNewPasswordHash(UUID id) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("SELECT NewPwdHash FROM harmony_auth_data WHERE UUID=?;");
preparedStatement.setString(1, id.toString());
ResultSet rs = preparedStatement.executeQuery();
rs.first();
preparedStatement.close();
connection.close();
return Objects.requireNonNullElse(rs.getString("NewPwdHash"), "");
} catch (SQLException e) {
putError(e);
return "";
}
}
@Nonnull
@Override
public Date getLastLoginTime(UUID id) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("SELECT LastLogin FROM harmony_auth_data WHERE UUID=?;");
preparedStatement.setString(1, id.toString());
ResultSet rs = preparedStatement.executeQuery();
rs.first();
preparedStatement.close();
connection.close();
String dateString = Objects.requireNonNullElse(rs.getString("LastLogin"), "1970-01-01 23:59:59");
return sdf.parse(dateString);
} catch (SQLException e) {
putError(e);
try {
return sdf.parse("1970-01-01 23:59:59");
} catch (ParseException e2) {
HarmonyAuthSMART.instance.getLogger().warning("这不可能!不可能出现这个错误!日期的读取失败了?");
e2.printStackTrace();
return new Date();
}
} catch (ParseException e) {
HarmonyAuthSMART.instance.getLogger().warning("这不可能!不可能出现这个错误!日期的读取失败了?");
e.printStackTrace();
return new Date();
}
}
// 上面方法相应的 set 方法
@Override
public void setPasswordHash(UUID id, String hash) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO harmony_auth_data (UUID, PwdHash, IForgotState, IForgotReason, NewPwdHash, LastLogin) VALUES (?, ?, false, '', '', '1970-01-01 23:59:59') ON DUPLICATE KEY UPDATE PwdHash=?;");
preparedStatement.setString(1, id.toString());
preparedStatement.setString(2, hash);
preparedStatement.setString(3, hash);
preparedStatement.execute();
preparedStatement.close();
connection.close();
} catch (SQLException e) {
putError(e);
}
}
@Override
public void setIForgotState(UUID id, boolean state) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO harmony_auth_data (UUID, PwdHash, IForgotState, IForgotReason, NewPwdHash, LastLogin) VALUES (?, '', ?, '', '', '1970-01-01 23:59:59') ON DUPLICATE KEY UPDATE IForgotState=?;");
preparedStatement.setString(1, id.toString());
preparedStatement.setBoolean(2, state);
preparedStatement.setBoolean(3, state);
preparedStatement.execute();
preparedStatement.close();
connection.close();
} catch (SQLException e) {
putError(e);
}
}
@Override
public void setIForgotManualReason(UUID id, String reason) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO harmony_auth_data (UUID, PwdHash, IForgotState, IForgotReason, NewPwdHash, LastLogin) VALUES (?, '', false, ?, '', '1970-01-01 23:59:59') ON DUPLICATE KEY UPDATE IForgotReason=?;");
preparedStatement.setString(1, id.toString());
preparedStatement.setString(2, reason);
preparedStatement.setString(3, reason);
preparedStatement.execute();
preparedStatement.close();
connection.close();
} catch (SQLException e) {
putError(e);
}
}
@Override
public void setIForgotNewPasswordHash(UUID id, String hash) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO harmony_auth_data (UUID, PwdHash, IForgotState, IForgotReason, NewPwdHash, LastLogin) VALUES (?, '', false, '', ?, '1970-01-01 23:59:59') ON DUPLICATE KEY UPDATE NewPwdHash=?;");
preparedStatement.setString(1, id.toString());
preparedStatement.setString(2, hash);
preparedStatement.setString(3, hash);
preparedStatement.execute();
preparedStatement.close();
connection.close();
} catch (SQLException e) {
putError(e);
}
}
@Override
public void setLastLoginTime(UUID id, Date date) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO harmony_auth_data (UUID, PwdHash, IForgotState, IForgotReason, NewPwdHash, LastLogin) VALUES (?, '', false, '', '', ?) ON DUPLICATE KEY UPDATE LastLogin=?;");
preparedStatement.setString(1, id.toString());
String dateString = sdf.format(date);
preparedStatement.setString(2, dateString);
preparedStatement.setString(3, dateString);
preparedStatement.execute();
preparedStatement.close();
connection.close();
} catch (SQLException e) {
putError(e);
}
}
@Override
public boolean isExist(UUID id) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("SELECT COUNT(UUID) FROM harmony_auth_data WHERE UUID=?");
preparedStatement.setString(1, id.toString());
ResultSet rs = preparedStatement.executeQuery();
rs.first();
preparedStatement.close();
connection.close();
return rs.getInt(1) != 0;
} catch (SQLException e) {
putError(e);
return false;
}
}
}
Statement
是 PreparedStatement
的简化版本,使用它只是我的习惯(都是笔者不好,真是对不起),各位在自己的插件中一定要尽量使用 PreparedStatement
啊。
我在 4-2 中提到过,executeQuery
中需要额外的参数,这里让你看看,如果不加,会有什么后果。
上面涉及到了许多 SQL 语句,实际上只有三种啦:创建表、读取、插入。
要注意的时,SQL 插值时,位置编号是从 1 开始的,这和数组从 0 开始不太一样。
需要注意的是,这里没有 USE test
这样的指令。
?> 到底怎么回事?
JDBC 在连接时就已经(为我们)选好了数据库。因为 USE
指令是管理员指令,应用程序不应该调用,我们也不用管那么多,总之,JDBC 连接成功后,就已经处于 USE <数据库名>
后的状态了。一切都已经准备好,我们直接操作数据表即可。
实现好这个接口后,数据存储就会很轻松啦!
每次重新创建 IDataManager
是为了避免 BukkitRunnable
异步执行可能发生的阻塞和访问冲突。
请原谅我起了这么长一个名字,这个管理器主要是管理像限制列表、OP 审核的进度、申请找回密码的玩家对话之类的信息,它们不用保存到数据库或文件中,需要在运行时管理。
创建类 RuntimeDataManager
:
package rarityeg.harmonyauthsmart;
import java.util.*;
public class RuntimeDataManager {
private static final List<UUID> RESTRICTS = new ArrayList<>();
private static final Map<UUID, Integer> IFORGOT_SETUP_MAP = new HashMap<>();
private static final List<UUID> READ_MODE_LIST = new ArrayList<>();
public synchronized static void addRestrictUUID(UUID id) {
RESTRICTS.add(id);
}
public synchronized static void removeRestrictUUID(UUID id) {
RESTRICTS.remove(id);
}
public synchronized static boolean hasRestrictUUID(UUID id) {
return RESTRICTS.contains(id);
}
public synchronized static void toReadMode(UUID id) {
READ_MODE_LIST.add(id);
}
public synchronized static void exitReadMode(UUID id) {
READ_MODE_LIST.remove(id);
}
public synchronized static boolean isInReadMode(UUID id) {
return READ_MODE_LIST.contains(id);
}
public synchronized static void toIForgotMode(UUID id, int mode) {
IFORGOT_SETUP_MAP.put(id, mode);
}
public synchronized static void exitIForgotMode(UUID id) {
IFORGOT_SETUP_MAP.remove(id);
}
public synchronized static int getIForgotMode(UUID id) {
return Objects.requireNonNullElse(IFORGOT_SETUP_MAP.get(id), 0);
}
}
READ_MODE_LIST
记录哪些 OP 正在审核玩家的密码恢复请求。IFORGOT_SETUP_MAP
记录哪些玩家正在请求密码恢复以及恢复到哪一步了。
审核模式下,OP 可以审核玩家的请求,此时禁用命令。
「IForgot」模式有两步,输入新密码和输入理由,此时禁用命令。
基于此创建了上面这些代码,应该非常简单。
这次我们基于 UUID 来管理玩家。
唯一出现的新知识点就是 synchronized
,它的意思是「同步」,也就是说,阻止多个线程同时访问一个对象,这很明显。ArrayList
不是线程安全(Thread Safe)的,因此要阻止它们同时被多个线程写入,稍微牺牲了一点性能但增加了安全性。至于 IDataManager
,它的实例属于各个线程,因此不影响。
我们暂且设计这些命令:
/hl <密码> <重复密码>
以注册,如果已经注册,则使用第一个密码登录。/hl <密码>
以登录,如果没有注册,认为「两次输入的密码不一致」。/iforgot
对普通玩家和登录前的 OP 是恢复密码,对登录后的 OP 是审核。
那么我们先来实现这些命令。创建类 plugin.yml
:
main: rarityeg.harmonyauthsmart.HarmonyAuthSMART
api-version: 1.16
version: 1.0
name: HarmonyAuth-SMART
database: true
description: "A third-party login plugin."
author: RarityEG
commands:
hl:
aliases:
- "l"
- "L"
- "reg"
- "register"
- "login"
- "log"
usage: "/hl <PASSWORD> [<PASSWORD>]"
description: "Login or register, judged automatically."
iforgot:
aliases:
- "ifg"
usage: "/iforgot"
description: "Create a IForgot request or (for OPs) start auditing."
这次由于是制作成品插件,我们为命令设定了很多别名。此外,我们设定了 database
选项。虽然没必要告知 Bukkit,但我觉得既然有这个选项,还是老老实实说出来比较好。
接下来我们需要完成命令处理器。还是和之前一样啦……
package rarityeg.harmonyauthsmart;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import javax.annotation.ParametersAreNonnullByDefault;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
public class CommandHandler implements CommandExecutor {
public static List<UUID> NoInterruptList = new ArrayList<>();
// 哪些玩家的命令正在执行中
@Override
@ParametersAreNonnullByDefault
public boolean onCommand(CommandSender commandSender, Command command, String label, String[] args) {
if (!(commandSender instanceof Player)) {
// 控制台登录个鬼
return false;
}
UUID id = ((Player) commandSender).getUniqueId();
// 强制转换并获取 UUID
if (RuntimeDataManager.getIForgotMode(id) != 0 || RuntimeDataManager.isInReadMode(id)) {
// 在审核模式或者恢复模式中,禁止命令
return true;
}
if (getIF(id)) {
commandSender.sendMessage(Util.getAndTranslate("msg.command-handling"));
// 当前尚有命令没有处理完成,拒绝处理
return true;
}
cli(id); // 暂时禁止该玩家执行命令
if (command.getName().equals("hl")) {
return onLoginCommand(commandSender, args);
// 分配到 onLoginCommand 中
} else if (command.getName().equals("iforgot")) {
if (!HarmonyAuthSMART.instance.getConfig().getBoolean("iforgot")) {
commandSender.sendMessage(Util.getAndTranslate("msg.iforgot-no-available"));
// 如果恢复模式被禁用
sti(id); // 允许该玩家继续执行命令
return true;
}
return onIForgotCommand(commandSender);
// 切换到 onIForgotCommand 继续执行
} else {
// 后备操作
sti(id);
// 开放命令
return false;
}
}
public boolean onLoginCommand(CommandSender commandSender, String[] args) {
Player player = (Player) commandSender;
UUID id = player.getUniqueId();
if (RuntimeDataManager.hasRestrictUUID(id)) {
// 进入异步处理
new BukkitRunnable() {
@Override
public void run() {
IDataManager idm; // 仅「占个位置」,下面按需赋值
if (HarmonyAuthSMART.instance.getConfig().getBoolean("mysql.enabled") && !HarmonyAuthSMART.dbError) {
// 数据库可用
idm = new DBDataManager();
} else {
// 数据库不可用
idm = new FileDataManager();
}
if (idm.isExist(id)) {
// 存在 ID
// 你看这样多方便,无论是数据库还是文件,都只需要调用 isExist
if (args[0] == null) {
// 没输密码
player.sendMessage(Util.getAndTranslate("msg.login-failed"));
sti(id); // 开放命令
List<String> hooks = Util.generateHooks("hook.on-login-failed", player.getName());
for (String cmd : hooks) {
// 按顺序循环 hooks 中的每项
Util.dispatchCommandAsServer(cmd);
// 以服务器身份执行命令
}
// 执行钩子
return;
}
if (idm.getPasswordHash(id).equals(Util.calculateMD5(args[0]))) {
RuntimeDataManager.removeRestrictUUID(id);
player.sendMessage(Util.getAndTranslate("msg.login-success"));
idm.setIForgotManualReason(id, "");
idm.setIForgotState(id, false);
idm.setLastLoginTime(id, new Date());
// 登录成功,重设日期,取消恢复请求
sti(id);
List<String> hooks = Util.generateHooks("hook.on-login-success", player.getName());
for (String cmd : hooks) {
Util.dispatchCommandAsServer(cmd);
}
return;
}
player.sendMessage(Util.getAndTranslate("msg.login-failed"));
List<String> hooks = Util.generateHooks("hook.on-login-failed", player.getName());
for (String cmd : hooks) {
Util.dispatchCommandAsServer(cmd);
}
sti(id);
return;
}
if (args.length < 2 || !args[0].equals(args[1])) {
player.sendMessage(Util.getAndTranslate("msg.register-failed"));
sti(id);
return;
}
idm.setPasswordHash(id, Util.calculateMD5(args[0]));
RuntimeDataManager.removeRestrictUUID(id);
player.sendMessage(Util.getAndTranslate("msg.register-success"));
idm.setIForgotManualReason(id, "");
idm.setIForgotState(id, false);
idm.setLastLoginTime(id, new Date());
sti(id);
List<String> hooks = Util.generateHooks("hook.on-register-success", player.getName());
for (String cmd : hooks) {
Util.dispatchCommandAsServer(cmd);
}
}
}.runTaskAsynchronously(HarmonyAuthSMART.instance);
} else {
player.sendMessage(Util.getAndTranslate("msg.login-success"));
// 已经登录
sti(id);
}
return true;
}
public boolean onIForgotCommand(CommandSender commandSender) {
Player player = (Player) commandSender;
UUID id = player.getUniqueId();
new BukkitRunnable() {
@Override
public void run() {
IDataManager idm;
if (HarmonyAuthSMART.instance.getConfig().getBoolean("mysql.enabled") && !HarmonyAuthSMART.dbError) {
idm = new DBDataManager();
} else {
idm = new FileDataManager();
}
if (!RuntimeDataManager.hasRestrictUUID(id)) {
if (!player.isOp()) {
idm.setIForgotState(id, false);
idm.setIForgotManualReason(id, "");
RuntimeDataManager.toIForgotMode(id, 1);
player.sendMessage(Util.getAndTranslate("msg.iforgot-newpwd"));
} else {
RuntimeDataManager.toReadMode(id);
player.sendMessage(Util.getAndTranslate("msg.audit-in"));
UUID firstId = idm.getNextRequest();
// 先获取一个请求,开始这个链式反应,参见 AC-1-2
if (firstId.equals(UUID.fromString("00000000-0000-0000-0000-000000000000"))) {
RuntimeDataManager.exitReadMode(id);
player.sendMessage(Util.getAndTranslate("msg.audit-out"));
} else {
player.sendMessage(Util.getAndTranslate("audit-uuid" + firstId.toString()));
player.sendMessage(Util.getAndTranslate("audit-reason" + idm.getIForgotManualReason(firstId)));
player.sendMessage(Util.getAndTranslate("audit-hint"));
}
}
} else {
RuntimeDataManager.toIForgotMode(id, 1);
player.sendMessage(Util.getAndTranslate("msg.iforgot-newpwd"));
}
sti(id);
}
}.runTaskAsynchronously(HarmonyAuthSMART.instance);
return true;
}
private static synchronized void cli(UUID id) {
NoInterruptList.add(id);
}
private static synchronized void sti(UUID id) {
NoInterruptList.remove(id);
}
private static synchronized boolean getIF(UUID id) {
return NoInterruptList.contains(id);
}
}
就这么多……啊不,不是。为了方便编写,我还创建了 Util
类:
package rarityeg.harmonyauthsmart;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.craftbukkit.libs.org.apache.commons.codec.binary.Hex;
import javax.annotation.Nonnull;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
public final class Util {
@Nonnull
public static String getAndTranslate(@Nonnull String key) {
String str = Objects.requireNonNullElse(HarmonyAuthSMART.instance.getConfig().getString(key, ""), "");
return ChatColor.translateAlternateColorCodes('&', str);
// 用于替换 & 为 §,方便服主配置
}
@Nonnull
public static String calculateMD5(@Nonnull String origin) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(origin.getBytes(StandardCharsets.UTF_8));
// 加码
return String.valueOf(Hex.encodeHex(md.digest()));
// 转换为十六进制
} catch (NoSuchAlgorithmException e) {
HarmonyAuthSMART.instance.getLogger().severe("必要的 MD5 哈希算法不可用,正在禁用本插件……。");
// 现代 CentOS 和 Windows Server 都有这个算法,应该不会有问题
e.printStackTrace();
Bukkit.getPluginManager().disablePlugin(HarmonyAuthSMART.instance);
// 紧急停止插件,此时返回任何值都不明智
return "";
}
}
// 用于生成命令钩子,后面会用到
public static List<String> generateHooks(@Nonnull String key, @Nonnull String playerName) {
List<String> origin = HarmonyAuthSMART.instance.getConfig().getStringList(key);
List<String> output = new ArrayList<>();
for (String cmd : origin) {
if (cmd != null && !cmd.equals("")) {
output.add(cmd.replaceAll("\\$\\{playerName}", playerName)); // 模板替换
}
}
return output;
}
public static synchronized void dispatchCommandAsServer(String cmd) {
new BukkitRunnable() {
@Override
public void run() {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd);
}
}.runTask(HarmonyAuthSMART.instance);
} // 执行命令
}
比较简单。有注释应该也比较容易看懂。
另外我们还将钩子读了出来,进行运行。
disablePlugin
用于停止插件,当出现不可恢复错误时才使用。
配置文件中不容易输入 §
符号,我们允许服主用 &
代替。
cli
和 sti
分别禁止命令执行和允许命令执行,我这么命名只是因为它们的功能和汇编中的 CLI
(Clear Interrupt,禁止中断)和 STI
(Set Interrupt,允许中断)指令很像啦。
这里我们借用了 Apache Commons CodeC 包中的工具。本来我们需要自己导入它,但该包已经成为 CraftBukkit 的一个库了,并且看上去无需反射就可以使用,我们就不麻烦 Maven 了。
笔者写到这里时已经有 5052 词了,本章节似乎太长啦,看来事件处理只能放在下一节了,有点可惜……
建议你在阅读之前把这些代码手动敲一遍,不长,你可以的!