跳到主要内容

8-2 设计插件功能

使用 TOTP 库

生成和验证密钥的算法很复杂,幸运的是,我们不需要动手写这样的算法,因为已经有人为我们完成了这项工作。

我们将使用 Java TOTP,它可以创建密钥和比对验证码,而且还能生成二维码,这足够让我们做完整个插件了!

用 Java TOTP 来创建密钥非常简单:

val secret = DefaultSecretGenerator().generate()

存储 TOTP 密钥

接下来我们需要存储玩家的 TOTP 密钥,你可能会想用简单方便的 PDC,或者试试刚刚学习的 MapDB,但它们都不是很适合这个插件:

  • PDC 中的数据与世界数据一并存储,当世界数据删除或重置后,存储的 TOTP 密钥会丢失。理想的情况是,除非服主跑路了,否则玩家的用户信息应当始终保持不变
  • MapDB 用来存储密钥这么小体量的数据不是很必要,为此将 MapDB 这么大一个库包含在插件中不是很划算。

与背包数据不同,这次我们要存储的数据量非常小,数据非常简单,对读写性能没什么要求,但是要容易修改和迁移。在这种情况下,使用 YAML 文件来存储就足够了。

你是职业选手吗?

你可能会觉得应当对 TOTP 密钥加密,但与密码不同,TOTP 密钥最终是要解密才能用于验证的。也就是说,存储 TOTP 密钥的安全性取决于其介质的安全性,而插件的配置文件是一个相对安全的介质 —— 如果有人能黑进服务器并访问文件,那最好先想想他们是怎么黑进来的。

我们可以选择配置文件来存储这些密钥,不过配置文件通常是供用户(而不是插件)修改的,我们最好另行创建一个文件,并在那里保存密钥。

尽管 config 属性只能用来访问 config.yml,不过 Bukkit 也提供了非常方便的方法来处理其它 YAML 文件:

val cfg = YamlConfiguration()
cfg.load(File(dataFolder, "secrets.yml"))  // 读取 secrets.yml 文件内容

// 和读写配置文件的方法一样
cfg.set("ThatRarityEG", "<Some TOTP Secret>")   // 写入值
println(cfg.getString("ThatRarityEG"))          // 读取值

cfg.save(File(dataFolder, "secrets.yml"))  // 保存文件

中间的读写部分与使用 config 属性的用法完全相同,至于读取和保存文件的部分,我们稍后将进行介绍。

显示图像

Java TOTP 库提供了生成二维码的功能,我们要如何将它展示给玩家呢?我们可以使用地图

建造过地图画的读者都知道,虽说地图的设计用途是显示周围地形,但只要放上合适的方块,就能改变地图上对应点的颜色!在插件开发中,我们当然也可以这么做,不过我们不需要放置一堆方块,因为 Bukkit 已经提供了直接在地图上绘画的功能,使用起来也非常方便:

mm.mapView?.addRenderer(
    object : MapRenderer() {
        override fun render(map: MapView, canvas: MapCanvas, player: Player) {
            // 在地图的 0, 0 处绘制指定图像
            canvas.drawImage(0, 0, img)
        }
    }
)

图像数据的生成可以由 Java TOTP 的输出转换而来。

防止暴力破解

虽说一下猜出 6 位验证码很难,不过如果使用脚本进行暴力尝试,还是有机会在 30 秒内找到正确的验证码的。

为了阻止这种事情发生,我们可以限制验证码输入的速率:每当玩家输入错误的验证码后,在几秒内,插件会忽略玩家进一步的消息。不过,这么做比较麻烦,我们有另一种简单的方法:如果玩家输入错误的验证码,就将玩家踢出服务器

重新连接服务器是一项很耗费时间的工作,而且受限于网络运载能力(以及与 Microsoft 验证身份所需的时间),坏人无法高频次地加入服务器并尝试验证码,我们可以利用这一道天然屏障来阻止暴力破解,不仅效果更好,还不需要在插件中额外添加太多代码。

命令处理

我们使用一个 /totp 命令来处理启用 TOTP 和验证 TOTP 两个操作。这么做的话,命令处理的逻辑会有点复杂,在开始编写代码前,我们最好先思考一下命令处理的流程:

  • 不含参数时(例如 /totp):
    1. 检查玩家是否已有密钥(不允许重复创建)。
    2. 为玩家生成新密钥。
    3. 生成二维码,并做成地图,放入玩家的物品栏。
  • 提供参数时(例如 /totp 135246):
    1. 检查玩家是否已有密钥(否则无从验证)。
    2. 检查玩家的验证码是否正确。
    3. 如果验证通过,就允许玩家执行命令。