Skip to main content
预览功能是服务器提供的功能,在最终确定其 API 之前,会将其暴露给扩展程序。声明了预览功能的扩展程序需要 vsql_allow_preview_extensions = ON 才能安装(请参阅启用预览层)——不使用预览功能的扩展程序无论此设置如何都会正常安装。
预览功能 API 并不稳定。基于预览功能构建的扩展程序在服务器更新后可能无法加载。当某个功能稳定后,其头文件将移动到版本化的稳定 C++ SDK 路径。

启用预览层

在安装任何使用预览功能的扩展程序之前,使用 SET PERSIST 设置 vsql_allow_preview_extensions = ON
对于此变量,SET GLOBAL 会被拒绝——服务器需要 SET PERSIST,以便设置在重启后仍然有效。具有预览功能的扩展程序在启动时加载,因此在服务器启动时,该变量必须为 ON。 如果您直接启动 mysqld(例如,从首次启动服务器的安装脚本中启动),请在命令行中传递该标志,而不是使用 mysqld-auto.cnf(因为此时 mysqld-auto.cnf 尚未存在,无法保存持久化值):
要禁用:
如果当前安装了任何使用预览功能的扩展程序,则此操作将失败。首先卸载这些扩展程序,然后关闭该设置。

功能索引

注册模式

要使用预览功能,请在文件范围内声明一个功能对象,并将其按引用传递给 make_extension() 中的 .with()。服务器将在注册期间填充对象的 abi 指针:
.with(capability) 告诉服务器扩展程序需要哪些功能。如果扩展程序在安装时 vsql_allow_preview_extensions 为 OFF,则服务器会拒绝安装,并显示一个指明该扩展程序名称的错误:ERROR 3219 (HY000): Failed to load VEF extension 'name': extension requires preview capabilities but vsql_allow_preview_extensions is OFF。该消息不会说明是哪项功能导致的。
扩展程序中声明的每个功能对象都必须精确地传递给 .with() 一次。在加载时,服务器会交叉检查每个声明的功能实例与 .with() 接收到的内容,如果违反了该规则,则 INSTALL EXTENSION 将失败:
  • 已声明但从未传递给 .with() capability '<Type>' was declared but never passed to .with(); every CapabilityBase-derived static must be registered via .with(cap) in the extension builder
  • 同一实例多次传递给 .with() capability '<Type>' passed to .with() more than once
  • 传递给 .with() 的对象不是功能: .with() received an object that does not inherit vsql::detail::CapabilityBase; not a registered capability
完整的错误消息为:Failed to load VEF extension '<name>': vef_register returned an error: <message above>

密钥环访问

密钥环功能 (vsql::preview::keyring) 允许扩展程序读取和写入存储在 MySQL 密钥环组件中的密钥。扩展程序可将其用于 API 密钥、加密密钥或其他不应存储在 SQL 表中的密钥。 功能名称 VEF_PREVIEW_KEYRING_NAME"vsql::preview::keyring" 为了使读取和写入成功,必须在 MySQL 服务器上安装密钥环组件。如果没有,则操作将返回 KeyringCapability::Status::UNAVAILABLE

状态值

KeyringCapability::Status 是一个作用域枚举,由 read()(在 ReadResult 中)和 write() 返回:

声明功能

包含头文件,在文件范围内声明一个功能对象,并将其传递给 .with()
服务器将在加载时填充 g_keyring 对象。如果未安装密钥环组件,则 read()write() 方法在运行时将返回 Status::UNAVAILABLE——请在每次调用时检查该状态,而不是基于单独的可用性探测。

读取和写入

data_id 是密钥标识符。auth_id 是所有者用户——传递一个空字符串(或在 read 上省略它,read 默认使用 {})以读取或写入与特定用户无关的内部密钥。 read 按值返回一个 ReadResult。使用结构化绑定将其绑定:
对于任何状态(Status::OK 除外),value 都是空的。 write 直接返回 Status,并将 data 存储在 data_id / auth_id 下。

完整示例

这是服务器 villagesql/test-extensions/ 目录中的 vsql_keyring_reader 测试扩展程序的简化版本。它注册了 2 个 VDF:keyring_readkeyring_store

MySQL 服务

mysql_services 功能 (vsql::preview::mysql_services) 允许扩展程序使用 MySQL 注册表服务——MySQL 组件所使用的正是这些服务,它们由已安装的组件或服务器核心提供。扩展程序在一处声明它需要的每一项服务;服务器在扩展程序加载时获取每一项服务,并在扩展程序卸载时释放它们。 功能名称 VEF_PREVIEW_MYSQL_SERVICES_NAME"vsql::preview::mysql_services" 当某项服务器设施没有自己的 VEF 功能时,请使用它。会话属性和密钥环自身的组件服务都可以通过这种方式访问。仅支持使用服务:将扩展程序自己的实现注册到注册表中是计划中的后续工作,不属于此功能的范围。

声明功能

在文件范围内声明一个 MysqlServices 对象,用 VSQL_REQUIRE_SERVICE 命名您使用的每一项服务,并将该对象传递给 .with()。请为每一项服务包含 MySQL 自己的头文件——服务的类型和方法都在该头文件中声明:
VSQL_REQUIRE_SERVICE(services, name, var) 声明 var(服务器将获取到的服务写入其中的引用),并在 services 上注册 name。它会为您将 var 声明为 staticMysqlServices 对象也需要是 static 的,您手动声明的任何引用同样如此:服务器在加载时通过它们写入,它们必须比扩展程序存活得更久。

固定特定实现

VSQL_REQUIRE_SERVICE 两次使用 name——既作为 C++ 的 SERVICE_TYPE(name),也作为服务器在注册表中查找的字符串。在该裸名称下,服务器获取该服务的默认实现。 要改为指定某一个实现,请使用其限定的注册表名称——service.component,即 MySQL 的 PROVIDES_SERVICE(component, service) 生成的形式。下面请求的是 component_keyring_file 组件提供的密钥环读取器,而不是默认实现:
限定名称的获取方式与裸名称相同,因此通常的规则同样适用:如果该确切实现未注册,则扩展程序将安装失败,而不会回退到另一个实现。

基于 MySQL 的头文件构建

服务定义属于 MySQL 的组件框架,而不属于 VEF,并且服务器不会安装它们。因此,mysql/components/services/*.h 不在扩展 SDK 中,也不在 make install 构建的任何内容中,其中包括发行版压缩包和 Docker 镜像。使用服务的扩展程序需要基于 VillageSQL 服务器源代码树构建: 树内测试扩展程序从 vsql_add_test_extension() 上的 MYSQL_HEADERS 标志获得这两者,该标志将它们作为 MYSQL_INCLUDE_DIRMYSQL_GENERATED_INCLUDE_DIR 传递。树外构建则设置自己的包含路径。 有两种构建失败出现的位置与导致它们的那一行不同。 遗漏某项服务的 MySQL 头文件会使 VSQL_REQUIRE_SERVICE 得到一个无法解析的名称,因此错误出现在该宏上,而不是出现在缺失的 include 上(clang 17):
某些服务定义使用了 size_t 却没有包含 <cstddef>,因此将其中某个头文件放在所有 villagesql 头文件之前,会在 MySQL 自己的头文件内部失败:
请像本页示例那样,首先包含 <cstddef>

调用服务

服务引用自身提供 valid(),而 -> 转发到服务。对引用使用 .,对服务使用 ->
在每次 -> 调用之前检查 valid()-> 返回获取到的指针,当服务未被获取时该指针为空。 获取失败的服务会导致安装失败,因此在一个正在运行的函数内部,所需的服务是有效的。这项检查仍然重要,因为手动声明且从未传递给 require()ServiceRef 永远不会被写入:它可以编译,扩展程序可以安装,而 valid() 在扩展程序的整个生命周期内都为 false。 服务是什么——它的方法、这些方法的参数以及返回值——由 MySQL 记录,而不是在此处记录。对于名为 NAME 的服务,请阅读服务器源代码树中的 include/mysql/components/services/NAME.h:其 BEGIN_SERVICE_DEFINITION(NAME) 块声明了每个方法,并附有各自的文档。请完全按照该头文件的规定调用它们,包括 MySQL 的约定:bool 返回 false 表示成功,返回 true 表示失败。

获取失败

每一项声明的服务都在扩展程序加载时获取,早于其任何函数被调用,因此未注册的服务会使加载失败,而不是稍后才浮现。INSTALL EXTENSION 会失败并指出该服务。 下面的 vsql_mysql_services_missing_test 是一个树内测试扩展程序,它要求一项注册表中不存在的服务。它不是您可以安装的东西——它是此失败被捕获的方式,也是当您自己的扩展程序要求此服务器不提供的服务时所产生的结果:
另有两种安装失败从此功能之外到达同一处:将 MysqlServices 对象排除在 .with() 之外,以及在 vsql_allow_preview_extensions 为 OFF 的服务器上安装。两者都在注册模式中介绍。

完整示例

vsql_mysql_services_session_test 的简化版本,位于服务器的 villagesql/test-extensions/ 树中。它通过组合两项服务,读取调用会话上正在运行的 SQL 命令:一项返回当前的 THD,另一项从中读取指定的属性。两者都是每台服务器上都注册的服务器核心服务,因此无需先安装任何东西:
安装它并调用该函数:

状态变量

status_var 功能 (vsql::status_var) 允许扩展程序将 long longdouble 计数器作为 MySQL 状态变量公开。扩展程序拥有存储空间并写入其中;服务器每次查询状态变量时,都会通过指针读取。 使用 vsql::preview_status_var::make_capability() 构建功能,并传递来自 make_int(name, value_ptr)make_double(name, value_ptr) 的大括号列表。模板从大括号列表中推断计数,因此不需要显式大小。

完整示例

make_int 需要一个 long long *make_double 需要一个 double *。这是支持的两种类型。

从 SQL 访问

INSTALL EXTENSION my_ext 之后,该变量将以扩展程序名称作为前缀显示:
来自多个查询线程的并发递增(使用非原子 ++)可能会偶尔丢失;这对于通过 SHOW STATUS 公开的近似调用计数器是可以接受的。

系统变量

sys_var 功能 (vsql::sys_var) 允许扩展程序注册由扩展程序拥有的存储空间支持的 MySQL 系统变量。支持四种类型:BOOL (bool *)、INT (long long *)、DOUBLE (double *) 和 STR (char **)。INTDOUBLE 描述符还携带 min_valmax_val 边界;所有描述符都携带默认值和注释。 使用 vsql::preview_sys_var::make_capability() 和相应的工厂函数 make_boolmake_intmake_doublemake_str 构建功能。功能对象还公开 get()set(),以便从扩展程序代码中进行编程访问。两者都返回 false 表示成功。 要响应值更改,请在描述符上链接 .on_change<&fn>()。回调将接收一个 sv::SysVarChange,其中包含 var_name() 和类型化的访问器 (as_int()as_real()as_str())。 服务器在持有其全局系统变量锁的同时调用该回调。在那里通过存储指针读取或写入本扩展程序的另一个变量是安全的,并且其他会话会立即看到新值,因为服务器也在同一把锁下读取这些变量。
调用该功能的 get()set()、运行 SQL,或者等待执行上述任一操作的线程,都会在该锁上死锁。请让回调保持简短且非阻塞,将需要 SQL 的工作交给线程工作器,或者在阻塞部分前后释放并重新获取 LOCK_global_system_variables,就像 sql/sys_vars.cc 中的 event_scheduler_update() 那样。
功能对象必须具有静态存储期。当用户设置变量时,MySQL 会直接写入存储指针。

完整示例

从 SQL 访问

安装 INSTALL EXTENSION my_ext 后,可以使用扩展名作为组件前缀来访问变量:

从扩展代码读取和写入

对于 INT 和 BOOL 变量,直接读取全局存储指针——MySQL 以原子方式更新这些变量。要通过 MySQL 更新变量(以便服务器处理锁定、范围验证和持久性),请调用 SYS_VARS.set(extension_name, var_name, scope, value)setget 都成功时返回 false。两者都不能从 on_change 回调中调用:它们都会在系统变量锁上死锁。
scope 参数控制持久性:

线程工作器

线程工作器功能 (vsql::preview::thread_worker) 允许扩展在服务器驱动的后台线程中运行。线程通过控制系统变量进行启动和停止,该变量由服务器在扩展加载时注册;服务器在定期计时器上、在文件描述符准备就绪时,或响应启用/禁用事件时调用扩展的工作函数。 功能名称 VEF_PREVIEW_THREAD_WORKER_NAME"vsql::preview::thread_worker"

声明功能

包含头文件,在文件范围内声明一个实例化了工作函数的 ThreadWorkerCapability,并将其传递给 .with()
工作函数作为非类型模板参数提供 (ThreadWorkerCapability<&my_work>),因此它必须是一个具有以下签名的函数。第一个构造函数参数是线程名称后缀;可选的第二个参数覆盖控制系统变量名称。

工作函数签名

reason 指示服务器调用该函数的原因。thread 是服务器拥有的此工作器的句柄(在初始 VEF_WAKEUP_ENABLE 调用时为 NULL——参见下文)。arg 是在描述符上注册的不透明指针;它会保持不变地传递。

唤醒生命周期

服务器使用以下四个原因之一调用工作函数: 当原因是 VEF_WAKEUP_ENABLE 时,thread 参数为 NULL,因为此时线程句柄尚不存在。对于其他三个原因,thread 不为 NULL。

唤醒返回值

工作函数返回一个 vef_next_wakeup_t,以更新下一个唤醒配置。任何字段中的零值都表示“保持当前设置”——返回一个值初始化的结构体 (return {};) 以保持两者不变。 要设置新的轮询文件描述符,请返回其值(必须大于零)。要清除现有的轮询文件描述符,请在 poll_fd 中返回 -1 当原因是 VEF_WAKEUP_DISABLE 时,返回值将被忽略。

线程名称和控制变量

描述符上的两个字段控制命名:
  • suffix——线程名称后缀。服务器将其与扩展名称连接起来,生成线程名称,如 my_ext/monitor
  • var_name——可选。如果非 NULL,服务器将注册此确切名称作为控制系统变量。如果为 NULL,服务器将使用默认模式 {suffix}_enabled
控制变量是服务器注册的系统变量,因此它以扩展名称作为组件前缀。对于后缀为 monitor 的扩展 my_ext,该变量为 my_ext.monitor_enabled 将其设置为 ON 会启动工作器:服务器先以 VEF_WAKEUP_ENABLE 调用工作函数,然后创建线程,因此该语句要等到首次调用完成后才返回。在工作器已经运行时再次将其设置为 ON 不会有任何效果。将其设置为 OFF 会在线程退出后才返回。服务器在这两种操作前后都会释放其全局系统变量锁,因此工作函数可以读取系统变量并运行 SQL。

完整示例

一个带有单个周期性工作器的最小扩展,该工作器在每个计时器周期中递增一个心跳计数器。
安装此扩展(并且 vsql_allow_preview_extensions = ON)后,服务器将在扩展名称下注册一个 heartbeat_enabled 系统变量。对于名为 my_ext 的扩展,使用以下命令启用工作器:

SQL 查询

sql_query 功能 (vsql::preview::sql_query) 允许扩展从后台线程执行 SQL 语句。查询在服务器内部通过功能 vtable 运行——扩展不链接到任何 MySQL 客户端库。 功能名称 VEF_PREVIEW_SQL_QUERY_NAME"vsql::preview::sql_query"
必须从线程工作器回调中使用该回调的 vef_thread_handle_t * 打开 SQL 会话。从 VDF 或从任意扩展创建的线程中调用 open() 是无效的——它需要工作器会话上下文。

声明功能

包含头文件,在文件范围内声明一个 SqlQueryCapability,并将其传递给 .with()。它通常与 ThreadWorkerCapability 一起注册,因为会话是从工作器回调中打开的:
g_sql.open(handle) 返回一个 Session。在使用之前,使用 operator bool 检查它;无效的 Session 表示功能 vtable 未绑定或服务器无法分配会话。Session 是只移动的,并在销毁时自行关闭。

执行查询

Session 通过 session.sql(sv) 生成一个 SqlQuery。查询可以以两种模式运行:
  • execute()——运行该语句并将完整的结果集缓冲到 Result 中。通过按调用方自己的速度调用 next() 来迭代行。
  • for_each(fn)——运行该语句,并为生成的每一行调用一次 fn,而不进行缓冲。返回的 Result 仅包含诊断信息(不包含行)。
两者都返回一个 Result。非 NULL 的 Result 并不意味着该语句已成功——调用 has_error() 以确定。 缓冲 (execute):
column_str() 返回一个 string_view,它仅在下一次 next() 调用或 Result 销毁之前有效。如果需要更长的生命周期,请复制它。string_view 具有 data() == nullptr 表示 SQL NULL。 流式 (for_each):
传递给回调的 Row 仅在调用期间有效——不要存储对它的引用,以便跨行使用。for_each 返回的 Result 不包含缓冲的行;在其上调用 next() 不会产生数据。仅将其用于 has_error()error()warning_count()warning(i)

诊断

execute()for_each() 都通过返回的 Result 提供诊断信息。一个诊断信息是一个 Diag
Result 暴露:
error() 在语句成功时返回一个默认构造的 Diag (errno_ == 0)。warning(i)i >= warning_count() 时返回一个默认构造的 Diag sqlstatemessage 视图指向由 Result 拥有的存储,并在 Result 销毁时失效——如果需要超出其生命周期,请复制它们。

完整示例

一个工作器,在每个周期执行一个带缓冲的查询和一个流式查询,并记录来自两者的诊断信息:

列存储

列存储允许扩展直接向 InnoDB 注册自定义的磁盘布局,用于其自定义类型之一,而不是将该类型的数据通过行的 VARBINARY 负载进行路由。当您的类型需要一种 VARBINARY 无法表达的磁盘布局时,请使用它——例如,必须存储在专用页中的打包浮点数组。这是一个功能特性:它启用了新的存储布局,而不是用于现有布局的调整旋钮。
列存储是一个预览版 ABI——正在积极开发中,并且可能在不同版本之间发生变化。它目前仅涵盖行级持久性;对自定义存储列进行索引尚未可用。

声明功能

两个预览功能协同工作:
  • vsql::preview::storage — 开放对 InnoDB 存储基础设施的访问(微事务、段、页)。在文件范围内声明一个 StorageCapability
  • vsql::preview::column_store — 将每个类型的存储实现绑定到扩展的自定义类型之一。在文件范围内使用 make_column_store<Ctx>(TYPE).…build() 声明一个 ColumnStoreCapability
两者都必须传递给 make_extension() 上的 .with()
make_column_store<MyCtx>(MY_TYPE) 将实现绑定到在同一扩展上注册的一个自定义类型。所有七个槽位都必须在 build() 时提供,因为每个槽位都映射到列生命周期中的一个不同点,InnoDB 在正常操作期间会到达该点。

七个存储函数

每个函数都接受 storage::Column::StorageCtx<MyCtx>*,其 user() 访问器返回扩展的每个列状态,并且其 arena() 提供服务器管理的辅助对象分配。每个函数在成功时返回 false,在出错时返回 true,并将消息写入 error_msg(容量 error_msg_len),以便将故障传递到 SQL 客户端。
mark_deletepurge 是不同的,因为 InnoDB MVCC 要求已删除的行在 purge 运行之前对旧的快照保持可读。

每个列的上下文和 Arena

C++ SDK 在调用 createload 之前,默认构造 MyCtx——在进入您的函数时,ctx->user() 已经填充。MyCtx 必须是默认可构造的;C++ SDK 使用不带参数的 T() 调用。 直接使用 ctx->user() 来初始化状态。不要调用 ctx->arena().construct<MyCtx>()——这将分配第二个、未使用的实例,并且 ctx->user() 不指向它。
load 遵循相同的模式——ctx->user() 预先填充,并且 storage_ref 携带由 ctx->set_ref()create 中存储的打包值:
仅使用 ctx->arena() 来分配辅助对象,这些对象太大或太动态,无法直接嵌入到 MyCtx 中。C++ SDK 在 drop 返回后,无论 drop 是否成功,都会自动销毁 arena 并调用 ~MyCtx()

InnoDB 访问实用程序

包含 <villagesql/preview/storage_api.h> 以获取 InnoDB 原语。所有页面的读取和写入都必须在微事务中进行:
提交微事务会释放页面锁,并写入重做日志记录,以使更改持久化。 create 时保留——有关完整的设置模式,请参见上面“每个列的上下文”中的 createload 示例。在 DML 操作期间,从根页面获取段引用以分配新页面:
页面 使用共享锁读取,使用独占锁写入。将 mtr_ref 传递给写入调用,以便 InnoDB 记录更改:
页面布局常量: 在标头或尾部区域内读取或写入会损坏页面——InnoDB 使用这些字节范围用于其自身的簿记和校验和。

语句事件

语句事件功能 (vsql::preview::statement_event) 在每个查询完成执行后运行一个扩展提供的处理程序。服务器在查询自己的线程上同步调用该处理程序,并传递执行元数据——查询文本、计时、行计数、连接标识以及优化器质量指标。可将其用于慢查询日志记录、审计或指标收集。 功能名称 VEF_PREVIEW_STATEMENT_EVENT_NAME"vsql::preview::statement_event"

声明功能

在文件范围内声明一个以触发阶段和处理程序函数实例化的 StatementEventCapability,并将其传递给 .with()
第一个模板参数是触发阶段,一个 vef_statement_event_phase_t 值。VEF_STATEMENT_EVENT_POSTEXECUTE 在查询完成执行后触发(无论成功还是失败),并且是此版本中唯一实现的阶段。其他 vef_statement_event_phase_t 值是保留的;为其中之一声明处理程序会导致服务器拒绝 INSTALL EXTENSION

处理程序参数

StatementEventArgs 是已完成查询的只读视图;在 POSTEXECUTE 阶段,每个字段都已填充。部分访问器: 由于 query() 在存在重写形式时返回服务器的重写形式,因此携带凭据的语句在送达处理程序时,其中的机密信息(如密码)已被混淆,而不是以明文形式呈现,这与常规日志、慢查询日志和二进制日志已经对它们进行脱敏的方式相匹配:SET PASSWORDCREATE/ALTER USER ... IDENTIFIED BYCHANGE REPLICATION SOURCE ... SOURCE_PASSWORD 以及 CREATE SERVER ... OPTIONS(PASSWORD ...)。没有重写规则的语句会逐字传递。 诸如 query()sqlstate()error_message() 之类的字符串访问器指向仅在处理程序调用期间有效的存储——如果您在处理程序返回后需要它们,请复制这些字节。 StatementEventResult::error_msg(fmt, ...) 写入一条 printf 格式化的消息。在 POSTEXECUTE 阶段,该消息是建议性的:服务器会记录它,但不会将其传播到客户端。

完整示例

vsql_slow_query_log 测试扩展的精简形式。它记录每个执行时间超过阈值的查询,将语句事件功能与系统变量结合以进行运行时配置:

从 SQL 启用

启用预览层后(请参阅启用预览层),安装该扩展并通过其系统变量进行配置:
每个比阈值慢的查询都会追加到配置的日志文件中:

身份验证方法

auth 功能 (vsql::preview::auth) 允许扩展程序提供一种服务器身份验证方法。账户通过 CREATE USER ... IDENTIFIED WITH <method-name> 选择使用它;在连接时,如果该名称不是已加载的 MySQL 认证插件,服务器会查询 VEF 认证注册表,并在握手过程中调用扩展程序的处理程序。当您需要针对服务器不了解的凭据来源(持有者令牌、外部身份提供方或自定义质询)对账户进行身份验证,而又不想编写 MySQL 认证插件时,请使用它。 功能名称 VEF_PREVIEW_AUTH_NAME"vsql::preview::auth" 该处理程序是一个类型化函数,它接收一个 AuthContext:它通过这个由服务器拥有的上下文读写握手数据包来与客户端通信,并且永远不会看到 MySQL 的内部认证结构。
认证结果是故障关闭的。服务器会将 AuthResult::kOk 以外的任何结果都视为拒绝连接——这里刻意没有“可能”或故障开放的结果。返回 AuthResult::kReject、返回 AuthResult::kError 或者从未设置有效账户的处理程序都会拒绝此次登录。

声明功能

包含头文件,编写一个类型化处理程序,使用流式的 make_auth<> 构建器构建一个描述符,并将该描述符交给一个 AuthCapability 令牌,再把该令牌传递给 .with()。预览功能头文件不属于 <villagesql/vsql.h> 总括头文件,因此请显式包含 <villagesql/preview/auth.h>
该构建器有六个组成部分: AuthCapability g_auth{descriptor} 是由 .with() 使用的自注册令牌。请将其声明为 static,以便它比注册过程存活得更久。 client_plugin 是可选的。make_auth 将通告的插件默认设置为 "mysql_clear_password"——每个 MySQL 客户端都附带的最低共同标准——因此从不调用 .client_plugin() 的方法仍然可以安装,简单的客户端仍然可以连接。调用 .client_plugin(name) 可以请求不同的插件;mysql_clear_password 会在密码槽位中原样接收持有者令牌。 如果客户端提供的插件不是该方法请求的插件,则它会被切换到所请求的插件并原样重发其凭据,这会耗费一次往返,并且需要客户端愿意进行该切换。.accepts_client_plugin(&callback) 让该方法可以改为保留客户端提供的插件:服务器将每个提供的名称传递给该回调,包括所请求的插件本身——无论回调返回什么,它都会被接受。未设置回调的方法不接受任何其他提议,因此其他每一个提议都会切换到所请求的插件。接受是最终的——服务器此后不会再切换回所请求的插件——因此只接受处理程序确实能解析其帧格式的插件。服务器在握手协商期间、在处理程序的首次读取之前查询该回调,因此它必须是一个纯谓词:没有数据包 I/O,不阻塞,没有副作用。

处理程序约定

处理程序符合 AuthHandler 类型——它接收一个 AuthContext & 并返回一个 AuthResult
它在握手期间于连接线程上同步调用。AuthContext 封装了服务器拥有的每次尝试的上下文;只在调用期间持有它,不要保留它。请调用它的方法,而不要通过函数表传递上下文指针。基于令牌的处理程序会使用的方法: 处理程序返回三种结果之一: AuthResult::kRejectAuthResult::kError 都会拒绝连接。只有 AuthResult::kOk 才会成功。 当处理程序将连接账户映射到另一个有效账户时——就像下面的示例将连接账户映射到 vsql_auth_test_user 那样——这就是代理,它需要 GRANT PROXY,与 MySQL 插件认证路径上的做法完全一样。

暂存活动角色

c.set_active_roles(roles, n_roles) 暂存应在会话上激活的角色,替代此次登录中账户的默认角色激活。roles 是一个包含 n_roles 个以 NUL 结尾名称的数组;这些字符串会被复制,因此调用方无需保留它们。服务器在账户解析之后应用它们,使用与 SET ROLE 相同的、经过授权检查的激活方式:只有确实授予了已认证账户的角色才会激活,未被授予的名称会被静默跳过——因此令牌永远无法授予或提升超出 DBA 所配置范围的权限。传递 n_roles == 0 则不激活任何角色(等同于 SET ROLE NONE)。

完整示例

一个最小的认证器,浓缩自服务器源代码树中 villagesql/test-extensions/vsql-auth-test/ 处的 vsql_auth_test 扩展程序,任何发行版都不包含它。它接受一个固定令牌,将连接映射到 vsql_auth_test_user,并请求 mysql_clear_password,以便令牌原样到达密码槽位。(树内扩展程序还添加了额外的令牌路径、一个 .accepts_client_plugin() 回调,以及下文介绍的两个选择项,用于驱动其测试套件。)

绑定账户并连接

在启用预览层后(请参阅启用预览层),安装该扩展程序并将一个账户绑定到该方法。由于处理程序会映射到第二个账户,因此也要创建该账户,并授予它 PROXY 权限,使连接账户可以取得其身份:
CREATE USER ... IDENTIFIED WITH vsql_auth_test 之所以被接受,是因为 vsql_auth_test 是一个已注册的 VEF 认证方法——与接受已安装插件名称的方式相同。 只有 IDENTIFIED WITH <method> 形式被接受,并可选地带 AS '...'。添加 BY '...' 是要求该方法将密码转换为存储的凭据——这是 MySQL 插件通过 generate_authentication_string() 完成的工作——而如今没有任何 VEF 认证方法声明该钩子,因此服务器会拒绝它:
绑定的方法名称会写入账户的 plugin 列,而不是表的默认值,该账户下次登录时读取的正是这一列:
该方法请求 mysql_clear_password,因此客户端必须传递 --enable-cleartext-plugin 才能以明文发送令牌。在令牌正确时,会话以映射后的账户身份运行,并通过 @@external_user 公开连接账户:
卸载该扩展程序会移除该方法;绑定到它的账户将无法再进行身份验证:

自动创建账户

一种方法也可以处理针对尚不存在账户的登录,并让服务器在登录成功时顺带创建该账户。如果没有这项功能,未知账户会在任何方法运行之前就被拒绝。 使用 .auto_create(&callback) 选择启用。该回调不接受参数并返回 bool;服务器在每次未知账户登录时调用它,而不是在注册时读取一次,因此该方法可以遵循它自己的运行时设置,而不必在扩展程序加载时就固定这一选择:
不使用 .auto_create(),或从回调返回 false,都会保持标准行为:未知账户被拒绝。同一时间只能有一个已安装的方法选择启用它——如果有两个返回 true,服务器不会去猜测,而是向错误日志记录一条警告,并像没有任何方法选择启用一样拒绝未知账户。 在处理程序中,c.account_unknown() 用于区分这两种情况。请先验证凭据,然后描述要创建什么并以其身份进行身份验证:
request_provision(account, roles, n_roles) 记录意图,不返回任何内容。服务器会在处理程序返回 AuthResult::kOk 之后自行运行 DDL,并且仅针对作为未知账户被路由进来的登录——因此处理程序随后拒绝的登录不会创建任何东西,而指定一个已存在账户的请求会被忽略。服务器运行的是 CREATE USER IF NOT EXISTS <account>@'%' IDENTIFIED WITH <method>,随后为每个指定的角色执行一条 GRANT:该账户始终为主机 % 创建并绑定到对其进行身份验证的方法,而且 account 不必是连接的用户名。如果无法完成创建——例如在 super_read_only 服务器上——则登录会失败,而不会在没有账户的情况下继续。 角色的行为与暂存活动角色中一致:角色归 DBA 所有。每个名称都必须已经作为可授予的角色存在,无法授予的角色会被记录并跳过,而不会使登录失败,因此令牌可以指定角色,但永远无法创建或提升角色。账户名来自客户端,因此服务器会将其作为标识符加引号——精心构造的名称只会变成一个名字古怪的账户,而绝不会变成第二条语句。 vsql_auth_test 扩展程序为连接用户配置角色 vsql_role_granted,并将这一选择项置于 vsql_auth_test.auto_create 之后,该变量初始为 OFF。先将它打开并创建该角色,然后以一个不存在的账户进行连接:
该账户现在已经存在,绑定到该方法,并持有已授予的角色:
错误的令牌仍然会故障关闭,并且不会配置任何东西:
选择启用会使未知账户与已存在账户之间的差别对任何持有有效凭据的人变得可观察,而标准的未知账户拒绝行为刻意隐藏了这一点。这是此功能所作的取舍;在为一个凭据被广泛持有的方法启用该选择项之前,请权衡这一点。

自动授予角色

默认情况下,令牌指定的角色只有在账户已经持有它时才会生效,而账户未持有的角色会被记录并跳过。.auto_grant(&callback) 改变了这一点:服务器会将暂存的角色授予该账户,因此由令牌决定会话获得哪些角色,而不仅仅是决定打开账户现有角色中的哪些。 该回调在形态上与 .auto_create() 一致——不接受参数,返回 bool,并且服务器在每次登录时调用它,因此它可以遵循一项运行时设置:
这两个选择项是相互独立的。.auto_create() 管辖针对不存在账户的登录;.auto_grant() 管辖向登录所解析到的账户授予角色,无论该账户是否是刚刚创建的。不使用 .auto_grant(),或返回 false,都会保持仅激活的默认行为。 该授予是持久的——它是一次普通的 GRANT,而不是仅限会话的激活——并且是累加的:服务器永远不会撤销令牌不再指定的角色。 vsql_auth_test 将其公开为 vsql_auth_test.auto_grant,同样初始为 OFF。它的 -token-roles 令牌暂存 vsql_role_grantedvsql_role_denied,而下面的账户两者都不持有。在该设置关闭时,登录不会改动该账户的角色:
以与上面相同的方式,用该令牌以 auth_user 身份连接,并查询哪些角色处于活动状态:
打开该设置并重复同一次登录:
现在两个角色都处于活动状态,并且 SHOW GRANTS 会显示服务器添加的授予:
.auto_grant() 打开时,一个有效的令牌就足以获得它所指定的任何角色。该角色必须已经存在,因此令牌仍然无法凭空创造权限,但决定账户可以取得哪些现有角色的不再是 DBA,而是该方法。