1 审计插件概述
🎑 数据库审计功能主要将用户对数据库的各类操作行为记录审计日志,以便日后进行跟踪、查询、分析,以实现对用户操作的监控和审计。
常见的审计插件有 MariaDB Audit Plugin、Percona Audit Log Plugin、McAfee MySQL Audit Plugin 三种。
🚀从MySQL5.5开始,内核中已经增加了一套server层的审计接口,添加了额外的审计埋点(hook)对所关心的地方进行事件捕获(event),并将捕获的事件传给审计插件进行处理。
审计插件对支持的事件按照设定的过滤规则和记录格式进行处理后,根据设定的日志记录方式,将审计日志记录到文件或系统日志中。
2 插件的观察者模式
2.1 观察者模式概念
🏭 Mysql中为了扩展方便,基本上很多模块都是通过插件的形式(Plugin)的方式加载到Mysql主程序上的,这其中不仅有一些日志、状态等插件,还有数据引擎等核心的插件。在Mysql中访问接口的方式主要有两类,一类是通过注册使用观察者模式来调用,另外一类就是数据库引擎通过这个handlerton的方式来实现。这里主要说明观察者模式。
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。观察者设计模式使订阅者能够从提供程序注册并接收通知。 它适用于需要基于推送的通知的任何方案。
2.2 mysql中观察者模式的实现——数据结构
1 两个类——Observer_info(观察者信息)和Delegate(代表<n>=把(工作、权力等)委托(给下级)<v>)
<1>Observer_info观察者类
//sql/rpl_handler.h .cc 观察者类
class Observer_info {
public:
void *observer;
st_plugin_int *plugin_int;
plugin_ref plugin; //插件指针解引用(返回**st_plugin_int),就是插件handle(句柄)
Observer_info(void *ob, st_plugin_int *p);
};
观察者类Observer_info。这个类中的成员包括了插件的handle。
实际观察者是五个执行的子类对应的结构体,其中全部都是函数指针,这里通过函数指针指向了具体的函数,实现了插件的功能。其中的函数指针指向的实际函数就是需要插件自己实现的。
Trans_observer 结构体
Server_state_observer 结构体
Binlog_transmit_observer 结构体
Binlog_relay_IO_observer 结构体
......
例如事务相关具体的观察者
Trans_observer trans_observer = {
sizeof(Trans_observer),
trans_before_dml, trans_before_commit, trans_before_rollback,
trans_after_commit, trans_after_rollback, trans_begin,
};
/* A handle of a plugin */
struct st_plugin_int {
LEX_CSTRING name{nullptr, 0};
st_mysql_plugin *plugin{nullptr}; //插件结构体指针、插件结构体入口
st_plugin_dl *plugin_dl{nullptr};
uint state{0};
uint ref_count{0}; /* number of threads using the plugin */ 插件使用线程数
void *data{nullptr}; /* plugin type specific, e.g. handlerton */插件具体类型
MEM_ROOT mem_root; /* memory for dynamic plugin structures */
sys_var *system_vars{nullptr}; /* server variables for this plugin */ 服务端为插件提供的变量
enum_plugin_load_option load_option{
PLUGIN_OFF}; /* OFF, ON, FORCE, F+PERMANENT */
};
下沉到工具的设计,这里的设计方式还可以,加一层句柄(工具)入口的思想。
/*
Plugin description structure.
*/
//st_mysql_plugin 就是struct_mysql_plugin的简称
struct st_mysql_plugin {
int type; /* the plugin type (a MYSQL_XXX_PLUGIN value) 插件类型 */
void *info; /* pointer to type-specific plugin descriptor 具体插件的结构体入口 */
const char *name; /* plugin name 插件名字 */
const char *author; /* plugin author (for I_S.PLUGINS) 插件作者 */
const char *descr; /* general descriptive text (for I_S.PLUGINS) 插件描述信息-输出提示信息类似 */
int license; /* the plugin license (PLUGIN_LICENSE_XXX) */
/** Function to invoke when plugin is loaded. */
int (*init)(MYSQL_PLUGIN);
/** Function to invoke when plugin is uninstalled. */
int (*check_uninstall)(MYSQL_PLUGIN);
/** Function to invoke when plugin is unloaded. */
int (*deinit)(MYSQL_PLUGIN);
unsigned int version; /* plugin version (for I_S.PLUGINS) */
/*`simple_status`和`system_variables`,它们公开了一些状态和系统变量。启用插件后,可以使用`SHOW`语句(`SHOW STATUS`,`SHOW VARIABLES`)或适当的Performance Schema表检查这些变量。*/
SHOW_VAR *status_vars; //插件在mysql中的表示状态
SYS_VAR **system_vars;
void *__reserved1; /* reserved for dependency checking */
unsigned long flags; /* flags for plugin */
};
以上,就把插件&观察者表示的差不多了。
<2>Delegate(代表<n>=把(工作、权力等)委托(给下级)<v>)行为的处理基类
class Delegate {
public:
typedef List<Observer_info> Observer_info_list; //大代表里面有五个小代表的组织形式
typedef List_iterator<Observer_info> Observer_info_iterator;
/**
Class constructor
@param key the PFS key for instrumenting the class lock
*/
explicit Delegate(
#ifdef HAVE_PSI_RWLOCK_INTERFACE
PSI_rwlock_key key
#endif
);
/**
Class destructor
*/
//大代表还有功能能
virtual ~Delegate();
//新增observer/插件
int add_observer(void *observer, st_plugin_int *plugin);
//移除
int remove_observer(void *observer);
//迭代器
Observer_info_iterator observer_info_iter();
//判断是否为空,用作三目判断
bool is_empty();
//读锁写锁
int read_lock();
int write_lock();
......................................
....................................
void lock_it(enum_delegate_lock_mode mode);
};
设计理念:观察者方式主要就是通过一个观察者类来监视行动,根据不同的行为来产生具体的代理类(事件类)对象并对事件进行处理。这也是一般设计上的一个事件处理的流程,通过代理或者委托来实际处理事件的运作。
🏳️🌈传统的设计方式是一串的函数调用,但是更抽象的机制上是对整个行动事件抽象封装成一个类。
比如说,事务操作相关、存储操作相关、日志操作相关.....☢️也就是插件中event下的那些class。
这样再看两个类就很清楚了,一定会是你中有我,我中有你,然后是通过很多排成队(List)的指针对象或者列表来调用。
在观察者类,又细分了个子类:
//以Trans_delegate为例
class Trans_delegate : public Delegate {
int before_dml(THD *thd, int &result);
int before_commit(THD *thd, bool all, Binlog_cache_storage *trx_cache_log,
Binlog_cache_storage *stmt_cache_log,
ulonglong cache_log_max_size, bool is_atomic_ddl);
int before_rollback(THD *thd, bool all);
int after_commit(THD *thd, bool all);
int after_rollback(THD *thd, bool all);
int trans_begin(THD *thd, int &result);
//事物相关,主要在事物逻辑中应用。它包含:before_dml,before_commit,before_rollback,after_commit,after_rollback,trans_begin等。
};
.....
//全局指针变量 哪几类
//事物相关,主要在事物逻辑中应用。它包含:before_dml,before_commit,before_rollback,after_commit,after_rollback,trans_begin等。
extern Trans_delegate *transaction_delegate;
//存储事件相关,主要用于日志同步。它主要有after_flush,after_sync。
extern Binlog_storage_delegate *binlog_storage_delegate;
//服务状态事件相关,用于监视服务状态启停等处。它主要有before_handle_connection,before_recovery,after_engine_recovery,after_recovery,
extern Server_state_delegate *server_state_delegate;
//传输事件相关,主要用于传输节点间事件。它主要有:transmit_start,
extern Binlog_transmit_delegate *binlog_transmit_delegate;
//Relay IO事件相关,主要用于主从复制。它主要有:thread_start,thread_stop,applier_start,applier_stop,before_request_transmit,after_read_event,after_queue_event ,after_reset_slave,applier_log_event
extern Binlog_relay_IO_delegate *binlog_relay_io_delegate;
......
-
extern Server_state_delegate *server_state_delegate; 会定义这几个子类的全局变量(入口)
-
子类下面的具体操作是函数,这些函数就是去找相关插件的hook函数
hook函数定义
/**
* This hook MUST be invoked after upgrade from .frm to data dictionary,使用条件
* @return 0 on success, >0 otherwise.
*/
int Server_state_delegate::after_dd_upgrade_from_57(THD *) {
DBUG_TRACE;
Server_state_param param;
int ret = 0;
FOREACH_OBSERVER(ret, after_dd_upgrade_from_57, (¶m));//FOREACH_OBSERVER宏函数
return ret;
}
FOREACH_OBSERVER宏函数
#define FOREACH_OBSERVER(r, f, thd, args)
......
Observer_info_iterator iter= observer_info_iter();
Observer_info *info= iter++;
for (; info; info= iter++)
{
······
if (((Observer *)info->observer)->f
&& ((Observer *)info->observer)->f args)
{
r = 1;
sql_print_error("Run function '" #f "' in plugin '%s' failed",
info->plugin_int->name.str);
break;
}
}
······
🌾 从中可以看出,这个for循环遍历整个链表,取出每个元素的指针并转换为Observer 类型的指针,再以传入的args作为参数调用其名字为f成员函数。这个f,就是各个delagate下的子类中的函数。
比如将宏进行文本替换,以after_dd_upgrade_from_57中调用的FOREACH_OBSERVER为例,外部的调用为:
FOREACH_OBSERVER(ret, after_dd_upgrade_from_57, (¶m));//FOREACH_OBSERVER宏函数
则内部for循环中,使用每个元素的指针封装出的调用可以翻译为:
if (((Observer *)info->observer)->after_dd_upgrade_from_57 &&
((Observer *)info->observer)->after_dd_upgrade_from_57 ((¶m)))
假设info指针指向的对象的after_sync函数成员存在,则以args作为这个函数的参数调用它。那么也就是说Server_state_delegate::after_dd_upgrade_from_57(THD *)的作用实际上是用来挨个调用链表中保存的各个Observer对象的after_dd_upgrade_from_57的成员函数。
2.2 mysql中观察者模式的实现——在数据结构上的操作<逆向>
<1>首先插件第一件事一定是注册,只有注册到相关的函数指针,具体的插件才会被调用
//例如插件注册到server_state_observer行为类中
int register_server_state_observer(Server_state_observer *observer,
void *plugin_var) {
DBUG_TRACE;
int result = server_state_delegate->add_observer(observer,(st_plugin_int *)plugin_var));
return result;
}
其实都是调用了Delegate类中add_observer方法
//sql/rpl_handler.cc文件
int Delegate::add_observer(void *observer, st_plugin_int *plugin) {
int ret = false;
if (!inited) return true; //已经初始化返回true,没有初始化就初始化
write_lock(); //添加写锁
//在Observer_info的迭代器中加入这个插件
Observer_info_iterator iter(observer_info_list);
Observer_info *info = iter++;
while (info && info->observer != observer) info = iter++;
if (!info) {
info = new Observer_info(observer, plugin);//这个插件的信息加入到迭代器
if (!info || observer_info_list.push_back(info, &memroot))
ret = true;
else if (this->use_spin_lock_type())
acquire_plugin_ref_count(info);
} else
ret = true;
unlock(); //解锁
return ret;
}
add_observer函数就是把插件的结构加入的对于的oberver中。
5个行为子类提供的注册方法,是能够被外部插件调用的。,插件可以通过5个行为子类提供的注册方法,去把自己注册到observer(观察者中)。这样他就在Observer_list中。FOREACH_OBSERVER宏函数遍历就可以拿到他的函数。
这样,就能把一个插件要关注哪些行为子类映射好。
<2>初始化,首先要初始化等相关:
\\初始化操作的类
int delegates_init()
{
······
transaction_delegate= new (place_trans_mem) Trans_delegate;
if (!transaction_delegate->is_inited())
{
sql_print_error("Initialization of transaction delegates failed. "
"Please report a bug.");
return 1;
}
//Binlog_strage_deleaget的
binlog_storage_delegate= new (place_storage_mem) Binlog_storage_delegate;
if (!binlog_storage_delegate->is_inited())
{
sql_print_error("Initialization binlog storage delegates failed. "
"Please report a bug.");
return 1;
}
//server_satae_delegate的
server_state_delegate= new (place_state_mem) Server_state_delegate;
······
}
这个函数专门初始化各种xxx_delegate类型的指针,为他们分配对象,实际上继续追踪调用栈可以发现如下函数调用
mysqld_main()
|
|
init_server_components() //初始化服务端组件们
|
|
delegates_init()
所以可以看出,实际上在mysql启动的时候这个这些指针就已经得到了初始化。
<3>注册、初始化以后的调用逻辑
在要使用插件的地方使用 RUN_HOOK:宏函数
-
RUN_HOOK:
//binlog存储
RUN_HOOK(binlog_storage, after_sync, (queue_head, log_file, pos))
......
//binlog传输
RUN_HOOK(binlog_transmit, reserve_header, (m_thd, flags, &m_packet))
........
//这里传入的参数就是delegat的那些子类中的方法🍊
//宏函数定义
#define RUN_HOOK(group, hook, args) \
(group##_delegate->is_empty() ? 0 : group##_delegate->hook args)
//一个三目运算,调用delegate中is_empty()函数,非空(插件的list非空)就把args参数出传入 那几个子类的相关的hook函数中。
实例如:
RUN_HOOK(binlog_storage, after_sync, (queue_head, log_file, pos))
binlog_storage_delegate->is_empty() ? 0 : binlog_storage_delegate->after_sync(queue_head, log_file, pos)
也就是说实际上是调用了binlog_storage_delegate指向的对象的函数来实现半同步的功能,如果is_empty()的返回值为真则不做任何操作,否则调用after_sync()函数来进行等待。binlog_storage_delegate这个指针的定义,可以在rpl_handler.cc中找到它,这是一个全局指针变量,类型为Binlog_storage_delegate ,就是那5几个全局变量,(已经在启动的时候就初始化)
after_sync( )这个函数实际上只是调用了FOREACH_OBSERVER这个宏:
-
通过宏FOREACH_OBSERVER遍历相关函数
#define FOREACH_OBSERVER(r, f, thd, args)
......
Observer_info_iterator iter= observer_info_iter();
Observer_info *info= iter++;
for (; info; info= iter++)
{
······
if (((Observer *)info->observer)->f
&& ((Observer *)info->observer)->f args)
{
r= 1;
sql_print_error("Run function '" #f "' in plugin '%s' failed",
info->plugin_int->name.str);
break;
}
}
······
从中可以看出,这个for循环遍历整个链表,取出每个元素的指针并转换为Observer 类型的指针,再以传入的args作为参数调用其名字为f成员函数。可以依然将宏进行文本替换,以Binlog_storage_delegate::after_sync中调用的FOREACH_OBSERVER为例,外部的调用为:
FOREACH_OBSERVER(ret, after_sync, thd, (¶m, log_file, log_pos));
则内部for循环中,使用每个元素的指针封装出的调用可以翻译为:
if (((Observer *)info->observer)->after_sync &&
((Observer *)info->observer)->after_sync ((¶m, log_file, log_pos)))
假设info指针指向的对象的after_sync函数成员存在,则以args作为这个函数的参数调用它。那么也就是说Binlog_storage_delegate::after_sync的作用实际上是用来挨个调用它链表中保存的各个Observer对象的after_sync的成员函数。
注意这里有一个类型转换,(Observer *)info,这个就具体到那个插件的信息上了。并不是说仍然还在Binlog_storage_delegate::after_sync这五个子类当中。
而半同步插件中等待ack的动作实际上再进一步由observer对象实现的,在Binlog_storage_delegate类中可以看到它的定义:
typedef Binlog_storage_observer Observer;
🍎 这里这样定义的原因是为了FOREACH_OBSERVER这个宏统一使用Observer 这个名字。那么实际上就是调用的Binlog_storage_observer的成员函数了。继续查看Binlog_storage_observer的定义:
typedef struct Binlog_storage_observer {
uint32 len;
/**
This callback is called after binlog has been flushed
This callback is called after cached events have been flushed to
binary log file but not yet synced.
@param param Observer common parameter
@param log_file Binlog file name been updated
@param log_pos Binlog position after update
@retval 0 Sucess
@retval 1 Failure
*/
int (*after_flush)(Binlog_storage_param *param,
const char *log_file, my_off_t log_pos);
int (*after_sync)(Binlog_storage_param *param,
const char *log_file, my_off_t log_pos); //after_sync的函数指针,这个函数是在五个子类下面的哪些hook函数中定义的
} Binlog_storage_observer;
/**
Binlog storage observer parameters
*/
typedef struct Binlog_storage_param {
uint32 server_id;
} Binlog_storage_param;
从上面可以看出,Binlog_storage_observer其实就是一个含有两个函数指针的class。
🍏 那么问题接下来就变成了到底Binlog_storage_delegate中保存Binlog_storage_observer实例是从哪里来的?Binlog_storage_observer中的函数指针具体又是指向哪个函数呢?
回到Binlog_storage_delegate类中,这个类有一个向链表中添加对象的add_observer函数,要向其中添加则应该会调用这个函数。查找对它的调用可以找到添加Binlog_storage_observer到链表中的函数register_binlog_storage_observer:
//rpl_handler
int register_binlog_storage_observer(Binlog_storage_observer *observer, void *p)
{
DBUG_ENTER("register_binlog_storage_observer");
int result= binlog_storage_delegate->add_observer(observer, (st_plugin_int *)p);
DBUG_RETURN(result);
}
🥕 而向其中添加的observer则又是register_binlog_storage_observer的参数,
那么顺着调用链继续向上找,最终可以在半同步插件的源码中找到semi_sync_master_plugin_init函数:
static int semi_sync_master_plugin_init(void *p)
{
#ifdef HAVE_PSI_INTERFACE
init_semisync_psi_keys();
#endif
my_create_thread_local_key(&THR_RPL_SEMI_SYNC_DUMP, NULL);
if (repl_semisync.initObject())
return 1;
if (ack_receiver.init())
return 1;
if (register_trans_observer(&trans_observer, p))
return 1;
if (register_binlog_storage_observer(&storage_observer, p)) //注册到binlog_storage_observer
return 1;
if (register_binlog_transmit_observer(&transmit_observer, p))//注册到binlog_transmit_observer
return 1;
return 0;
}
这个函数注册半同步插件的各个xxxx_obeserver,将其添加到xxxx_delegate的链表中以供调用。依然关注之前的Binlog_storage_observe,上文提到的register_binlog_storage_observer函数参数来自于一个叫storage_observer的变量,这个变量就是Binlog_storage_observe的一个实例。继续追踪最后可以发现这是一个在半同步复制插件源码中定义的全局变量:
//semisync_master_plugin.cc
Binlog_storage_observer storage_observer = {
sizeof(Binlog_storage_observer), // len
repl_semi_report_binlog_update, // report_update
repl_semi_report_binlog_sync, // after_sync agter_sync就是在这里,之后会执行插件中的逻辑
};
具体的repl_semi_report_binlog_sync是在注册的时候把值传到after_sync。
after_sync指针指向的repl_semi_report_binlog_sync就是半同步插件关于等待从库ack的具体实现。
3 audit_null审计插件的实现
🌾 从MySQL5.5开始,内核中已经增加了一套server层的审计接口,添加了额外的审计埋点对用户所关心的地方进行事件捕获,并将捕获的事件传给审计插件进行处理。
审计插件对支持的事件按照设定的过滤规则和记录格式进行处理后,根据设定的日志记录方式,将审计日志记录到文件或系统日志中。
此外,发生可审核事件时,服务器中的多个位置会调用审核接口,以便在必要时可以向注册的审核插件通知该事件。要参见发生此类调用的位置,请在服务器源文件中搜索形式为的函数调用。发生以下服务器操作的审核通知:mysql_audit_xxx()
,audit_null调用的就是mysql_audit_null()
。
-
客户端连接和断开事件
-
将消息写入常规查询日志(如果已启用日志)
-
将消息写入错误日志
-
发送查询结果给客户端
3.1 audit_null插件结构
参考:https://www.docs4dev.com/docs/zh/mysql/5.7/reference/writing-audit-plugins.html,官方文档
根据插件的功能和要求,可能还需要其他MySQL或常规头文件。
#include <mysql/plugin_audit.h>
plugin_audit.h
include plugin.h
,因此无需显式包括后者。plugin.h
定义MYSQL_AUDIT_PLUGIN
服务器插件类型和声明插件所需的数据结构。plugin_audit.h
定义特定于审核插件的数据结构。
-
审核插件通用描述符
-
审核插件类型专用描述符
-
审核插件通知功能
-
审核插件错误处理
-
审核插件使用情况
审核插件通用描述符(所有插件都通用的)
审核插件与任何MySQL服务器插件一样,具有通用的插件描述符(请参见“服务器插件库和插件描述符”)和特定于类型的插件描述符。在中audit_null.c
,的一般描述符audit_null
如下所示:
/*
Plugin library descriptor
*/
//插件描述
mysql_declare_plugin(audit_null){
MYSQL_AUDIT_PLUGIN, /* type */
&audit_null_descriptor, /* descriptor */
"NULL_AUDIT", /* name */
PLUGIN_AUTHOR_ORACLE, /* author */
"Simple NULL Audit", /* description */
PLUGIN_LICENSE_GPL,
audit_null_plugin_init, /* init function (when loaded) */
nullptr, /* check uninstall function */
audit_null_plugin_deinit, /* deinit function (when unloaded) */
0x0003, /* version */
simple_status, /* status variables */
system_variables, /* system variables */
nullptr,
0,
} mysql_declare_plugin_end;
第一个成员MYSQL_AUDIT_PLUGIN
标识此插件为审核插件。
audit_null_descriptor
指向特定于类型的插件描述符,稍后将进行描述。
所述name
构件(NULL_AUDIT
)指示要用于如在语句中的插件的引用名称INSTALL PLUGIN
或UNINSTALL PLUGIN
。这也是由INFORMATION_SCHEMA.PLUGINS
或显示的名称SHOW PLUGINS
。
该audit_null_plugin_init
被加载插件时初始化函数执行插件初始化。audit_null_plugin_deinit
插件卸载后,该函数执行清理。
通用插件描述符还引用simple_status
和system_variables
,它们公开了一些状态和系统变量。启用插件后,可以使用SHOW
语句(SHOW STATUS
,SHOW VARIABLES
)或适当的Performance Schema表检查这些变量。
该simple_status
结构声明了几个状态变量,其形式为。为收到的每个通知增加状态变量。其他状态变量则更具体,仅针对特定事件的通知将其递增。Audit_null_*xxx*``NULL_AUDIT``Audit_null_called``NULL_AUDIT
system_variables`是系统变量元素的数组,每个元素都是使用宏定义的。这些系统变量的名称为形式。这些变量可用于在运行时与插件通信。`MYSQL_THDVAR_*xxx*``null_audit_*xxx*
审核插件类型专用描述符(特定插件可以自定义的)
audit_null_descriptor
常规插件描述符中的值指向特定于类型的插件描述符。对于审核插件,此描述符具有以下结构(在plugin_audit.h
中定义):
struct st_mysql_audit
{
int interface_version;
void (*release_thd)(MYSQL_THD);
int (*event_notify)(MYSQL_THD, mysql_event_class_t, const void *);
unsigned long class_mask[MYSQL_AUDIT_CLASS_MASK_SIZE];
};
审计插件的特定于类型的描述符具有以下成员:
-
interface_version
:按照惯例,特定于类型的插件描述符以给定插件类型的接口版本开头。服务器检查interface_version
何时加载插件,以参见插件是否与其兼容。对于审核插件,interface_version
成员的值是MYSQL_AUDIT_INTERFACE_VERSION
(在中定义plugin_audit.h
)。 -
release_thd
:服务器调用的一个函数,用于通知插件它正在与其线程上下文分离。如果没有这样的功能,则应为NULL
-
event_notify
:服务器调用此功能以通知插件已发生可审核的事件。这个功能不应该NULL
;这是没有意义的,因为不会进行审核。 -
class_mask
:MYSQL_AUDIT_CLASS_MASK_SIZE
元素数组。每个元素为给定的事件类指定一个位掩码,以指示插件要为其通知的子类。(这是插件“订阅”感兴趣事件的方式。)元素应为0,以忽略相应事件类的所有事件。
🐰 服务器将
event_notify
和release_thd
功能一起使用。在特定线程的上下文中调用它们,并且线程可能执行产生多个事件通知的活动。服务器第一次调用event_notify
线程时,它将创建插件与线程的绑定。存在此绑定时,无法卸载该插件。当线程的其他事件不再发生时,服务器会通过调用release_thd
函数,然后破坏绑定。例如,当客户发出一条语句时,处理该语句的线程可能会通知审计插件有关该语句产生的结果集和正在记录的语句的信息。发生这些通知后,服务器会释放插件,然后将线程置于睡眠状态,直到客户端发出另一条语句。
这种设计使插件可以在第一次调用event_notify
函数时分配给定线程所需的资源,并在函数中释放它们release_thd
:
event_notify function:
if memory is needed to service the thread
allocate memory
... rest of notification processing ...
release_thd function:
if memory was allocated
release memory
... rest of release processing ...
这比在通知功能中重复分配和释放内存更有效。
对于NULL_AUDIT
审核插件,特定于类型的插件描述符如下所示:
static struct st_mysql_audit audit_null_descriptor=
{
MYSQL_AUDIT_INTERFACE_VERSION, /* interface version 版本 */
NULL, /* release_thd function */
//注意aduti_null_notify也定义在了这个数据结构中
audit_null_notify, /* notify function */
{ (unsigned long) MYSQL_AUDIT_GENERAL_ALL,
(unsigned long) MYSQL_AUDIT_CONNECTION_ALL,
(unsigned long) MYSQL_AUDIT_PARSE_ALL,
(unsigned long) MYSQL_AUDIT_AUTHORIZATION_ALL,
(unsigned long) MYSQL_AUDIT_TABLE_ACCESS_ALL,
(unsigned long) MYSQL_AUDIT_GLOBAL_VARIABLE_ALL,
(unsigned long) MYSQL_AUDIT_SERVER_STARTUP_ALL,
(unsigned long) MYSQL_AUDIT_SERVER_SHUTDOWN_ALL,
(unsigned long) MYSQL_AUDIT_COMMAND_ALL,
(unsigned long) MYSQL_AUDIT_QUERY_ALL,
(unsigned long) MYSQL_AUDIT_STORED_PROGRAM_ALL }
};
服务器调用audit_null_notify()
以将审核事件信息传递给插件。插件没有release_thd
功能。
class_mask
成员是一个数组,用于指示插件预订的事件类。
下面class_mask
构件是一个数组指示哪些事件类插件订阅了。数组内容订阅了所有可用事件类的所有子类。要忽略给定事件类的所有通知,请将相应的class_mask
元素指定为0。
class_mask
元素的数量与事件类的数量相对应,事件类的数量在plugin_audit.h
中定义的mysql_event_class_t
枚举中列出:
typedef enum
{
MYSQL_AUDIT_GENERAL_CLASS = 0,
MYSQL_AUDIT_CONNECTION_CLASS = 1,
MYSQL_AUDIT_PARSE_CLASS = 2,
MYSQL_AUDIT_AUTHORIZATION_CLASS = 3,
MYSQL_AUDIT_TABLE_ACCESS_CLASS = 4,
MYSQL_AUDIT_GLOBAL_VARIABLE_CLASS = 5,
MYSQL_AUDIT_SERVER_STARTUP_CLASS = 6,
MYSQL_AUDIT_SERVER_SHUTDOWN_CLASS = 7,
MYSQL_AUDIT_COMMAND_CLASS = 8,
MYSQL_AUDIT_QUERY_CLASS = 9,
MYSQL_AUDIT_STORED_PROGRAM_CLASS = 10,
/* This item must be last in the list. */
MYSQL_AUDIT_CLASS_MASK_SIZE
} mysql_event_class_t;
对于任何给定的事件类,plugin_audit.h
定义各个事件子类的位掩码符号,以及xxx_ALL
符号,它是所有子类位掩码的并集。例如,对于MYSQL_AUDIT_CONNECTION_CLASS
(涵盖连接和断开事件的类),plugin_audit.h
定义以下符号:
typedef enum
{
/** occurs after authentication phase is completed. */
MYSQL_AUDIT_CONNECTION_CONNECT = 1 << 0,
/** occurs after connection is terminated. */
MYSQL_AUDIT_CONNECTION_DISCONNECT = 1 << 1,
/** occurs after COM_CHANGE_USER RPC is completed. */
MYSQL_AUDIT_CONNECTION_CHANGE_USER = 1 << 2,
/** occurs before authentication. */
MYSQL_AUDIT_CONNECTION_PRE_AUTHENTICATE = 1 << 3
} mysql_event_connection_subclass_t;
#define MYSQL_AUDIT_CONNECTION_ALL (MYSQL_AUDIT_CONNECTION_CONNECT | \
MYSQL_AUDIT_CONNECTION_DISCONNECT | \
MYSQL_AUDIT_CONNECTION_CHANGE_USER | \
MYSQL_AUDIT_CONNECTION_PRE_AUTHENTICATE)
要订阅连接事件类的所有子类(如NULL_AUDIT
插件一样),插件将MYSQL_AUDIT_CONNECTION_ALL
在相应的class_mask
元素中指定(class_mask[1]
在这种情况下)。要仅订阅某些子类,插件会将class_mask
元素设置为感兴趣的子类的并集。例如,仅订阅connect和change-user子类,插件将设置class_mask[1]
为以下值:
MYSQL_AUDIT_CONNECTION_CONNECT | MYSQL_AUDIT_CONNECTION_CHANGE_USER
审核插件通知功能
审核插件的大部分工作都在通知功能(event_notify
特定于类型的插件描述符的成员)中进行。服务器为每个可审核事件调用此函数。审核插件通知功能具有以下原型:
int (*event_notify)(MYSQL_THD, mysql_event_class_t, const void *);
event_notify
函数原型的第二个和第三个参数表示事件类和指向事件结构的通用指针。(不同类中的事件具有不同的结构。通知函数可以使用事件类的值来确定采用哪种事件结构。)该函数处理事件并返回一个状态,指示服务器是应继续处理该事件还是终止该事件。
对于NULL_AUDIT
,通知功能为audit_null_notify()
。此函数增加一个全局事件计数器(插件将其公开为Audit_null_called
状态值),然后检查事件类以确定如何处理事件结构:
static int audit_null_notify(MYSQL_THD thd __attribute__((unused)),
mysql_event_class_t event_class,
const void *event)
{
...
number_of_calls++;
if (event_class == MYSQL_AUDIT_GENERAL_CLASS)
{
const struct mysql_event_general *event_general=
(const struct mysql_event_general *)event;
...
}
else if (event_class == MYSQL_AUDIT_CONNECTION_CLASS)
{
const struct mysql_event_connection *event_connection=
(const struct mysql_event_connection *) event;
...
}
else if (event_class == MYSQL_AUDIT_PARSE_CLASS)
{
const struct mysql_event_parse *event_parse =
(const struct mysql_event_parse *)event;
...
}
...
}
通知函数event
根据的值解释参数event_class
。该event
参数是指向事件记录的通用指针,事件记录的结构因事件类而异。(该plugin_audit.h
文件包含定义每个事件类内容的结构。)对于每个类,audit_null_notify()
将事件强制转换为适当的类特定结构,然后检查其子类以确定要递增的子类计数器。例如,用于处理connection-event类中的事件的代码如下所示:
else if (event_class == MYSQL_AUDIT_CONNECTION_CLASS)
{
const struct mysql_event_connection *event_connection=
(const struct mysql_event_connection *) event;
switch (event_connection->event_subclass)
{
case MYSQL_AUDIT_CONNECTION_CONNECT:
number_of_calls_connection_connect++;
break;
case MYSQL_AUDIT_CONNECTION_DISCONNECT:
number_of_calls_connection_disconnect++;
break;
case MYSQL_AUDIT_CONNECTION_CHANGE_USER:
number_of_calls_connection_change_user++;
break;
case MYSQL_AUDIT_CONNECTION_PRE_AUTHENTICATE:
number_of_calls_connection_pre_authenticate++;
break;
default:
break;
}
}
注意
一般事件类(
MYSQL_AUDIT_GENERAL_CLASS
)已弃用,并将在以后的MySQL版本中删除。为了减少插件开销,最好只订阅感兴趣的特定事件类。
对于某些事件类,该NULL_AUDIT
插件除了增加计数器外,还执行其他处理。无论如何,当通知功能完成事件处理后,它应返回一个状态,指示服务器是应继续处理该事件还是终止该事件。
审核插件使用前环境
要编译和安装插件库文件,请使用“编译和安装插件库”中的说明。要使该库文件可供使用,请将其安装在插件目录(由plugin_dir
系统变量命名的目录)中。对于该NULL_AUDIT
插件,当您从源代码构建MySQL时,将对其进行编译和安装。它也包含在二进制发行版中。构建过程将生成一个共享对象库,其名称为adt_null.so
(.so
后缀可能因平台而异)。
要在运行时注册插件,请使用以下语句(.so
根据需要调整平台的后缀):
INSTALL PLUGIN NULL_AUDIT SONAME 'adt_null.so';
要验证插件安装,请检查INFORMATION_SCHEMA.PLUGINS
表或使用以下SHOW PLUGINS
语句。
虽然NULL_AUDIT
安装了审计插件,它暴露状态变量将指示该插件被称为事件:
mysql> SHOW STATUS LIKE 'Audit_null%';
+---------------------------------------- +-------- +
| Variable_name | Value |
+---------------------------------------- +-------- +
| Audit_null_authorization_column | 0 |
| Audit_null_authorization_db | 0 |
| Audit_null_authorization_procedure | 0 |
| Audit_null_authorization_proxy | 0 |
| Audit_null_authorization_table | 0 |
| Audit_null_authorization_user | 0 |
| Audit_null_called | 185547 |
| Audit_null_command_end | 20999 |
.............................
| Audit_null_server_shutdown | 0 |
| Audit_null_server_startup | 1 |
| Audit_null_table_access_delete | 104 |
| Audit_null_table_access_insert | 2839 |
| Audit_null_table_access_read | 97842 |
| Audit_null_table_access_update | 278 |
+---------------------------------------- +-------- +
Audit_null_called
计算所有事件,其他变量计算特定事件子类的实例。例如,SHOW STATUS
如果启用了该语句,则上述语句使服务器将结果发送给客户端,并向通用查询日志中写入一条消息。因此,客户端发出的声明重复导致Audit_null_called
,Audit_null_general_result
以及Audit_null_general_log
将每次递增。无论是否启用该日志,都会发生通知。
插件的具体使用
状态变量值是全局的,并且在所有会话中汇总。没有用于单个会话的计数器。
NULL_AUDIT
公开几个可在运行时与插件通信的系统变量:
mysql> SHOW VARIABLES LIKE 'null_audit%';
+--------------------------------------------------- +------- +
| Variable_name | Value |
+--------------------------------------------------- +------- +
| null_audit_abort_message | |
| null_audit_abort_value | 1 |
| null_audit_event_order_check | |
| null_audit_event_order_check_consume_ignore_count | 0 |
| null_audit_event_order_check_exact | 1 |
| null_audit_event_order_started | 0 |
| null_audit_event_record | |
| null_audit_event_record_def | |
+--------------------------------------------------- +------- +
☢️ 该NULL_AUDIT
系统变量有下列含义:
-
null_audit_abort_message
:事件中止时使用的自定义错误消息。 -
null_audit_abort_value
:事件中止时使用的自定义错误代码。 -
null_audit_event_order_check
:事件匹配之前,预期的事件顺序。事件匹配后,匹配结果。 -
null_audit_event_order_check_consume_ignore_count
:事件匹配不应消耗匹配事件的次数。 -
null_audit_event_order_check_exact
:事件匹配是否必须精确。禁用此变量将启用跳过事件null_audit_event_order_check
顺序匹配期间未列出的事件。在指定的事件中,它们仍必须按照给定的顺序进行匹配。 -
null_audit_event_order_started
:供内部使用。 -
null_audit_event_record
:发生事件记录后记录的事件。 -
null_audit_event_record_def
:记录事件时要匹配的开始和结束事件的名称,以分号分隔。必须在每个记录事件的语句之前设置该值。
为了演示这些系统变量的用法,假设db1.t1
存在一个表,其创建如下:
CREATE DATABASE db1;
CREATE TABLE db1.t1 (a VARCHAR(255));
为了进行测试,可以记录通过插件传递的事件。要开始记录,请在null_audit_event_record_def
变量中指定开始和结束事件。
例如:
SET @@null_audit_event_record_def =
'MYSQL_AUDIT_COMMAND_START;MYSQL_AUDIT_COMMAND_END';
在发生与那些开始和结束事件匹配的语句之后,null_audit_event_record
系统变量将包含结果事件序列。例如,
🍎 注意:在记录
SELECT 1
语句的事件之后,null_audit_event_record
是一个字符串,其值包含一组事件字符串:MYSQL_AUDIT_COMMAND_START;command_id="3";
MYSQL_AUDIT_PARSE_PREPARSE;;
MYSQL_AUDIT_PARSE_POSTPARSE;;
MYSQL_AUDIT_GENERAL_LOG;;
MYSQL_AUDIT_QUERY_START;sql_command_id="0";
MYSQL_AUDIT_QUERY_STATUS_END;sql_command_id="0";
MYSQL_AUDIT_GENERAL_RESULT;;
MYSQL_AUDIT_GENERAL_STATUS;;
MYSQL_AUDIT_COMMAND_END;command_id="3";
例如记录事件的INSERT INTO db1.t1 VALUES('some data')
语句后,null_audit_event_record
具有以下值:
MYSQL_AUDIT_COMMAND_START;command_id="3";
MYSQL_AUDIT_PARSE_PREPARSE;;
MYSQL_AUDIT_PARSE_POSTPARSE;;
MYSQL_AUDIT_GENERAL_LOG;;
MYSQL_AUDIT_QUERY_START;sql_command_id="5";
MYSQL_AUDIT_TABLE_ACCESS_INSERT;db="db1" table="t1"; //这里有具体的数据
MYSQL_AUDIT_QUERY_STATUS_END;sql_command_id="5";
MYSQL_AUDIT_GENERAL_RESULT;;
MYSQL_AUDIT_GENERAL_STATUS;;
MYSQL_AUDIT_COMMAND_END;command_id="3";
每个事件字符串都有这种格式,用分号分隔字符串部分:
event_name;event_data;command
事件字符串包含以下部分:
-
event_name
:事件名称(以开头的符号MYSQL_AUDIT_
)。 -
event_data
:为空,或如稍后所述,与事件相关的数据。 -
command
:为空,或如稍后所述,在事件匹配时执行的命令。
注意:null_audit并不是有记忆的,一次会话结束变量重新为空
该
NULL_AUDIT
插件的局限性在于事件记录仅适用于单个会话。在给定会话中记录事件后,后续会话中的事件记录将产生null_audit_event_record
值NULL
。要再次记录事件,必须重新启动插件。
要检查审核API调用的顺序,请将null_audit_event_order_check
变量设置为特定操作的预期事件顺序,列出一个或多个事件字符串,每个事件字符串内部包含两个分号,其他分号分隔相邻的事件字符串:
event_name;event_data;command [;event_name;event_data;command] ...
例如:
SET @@null_audit_event_order_check =
'MYSQL_AUDIT_CONNECTION_PRE_AUTHENTICATE;;;'
'MYSQL_AUDIT_GENERAL_LOG;;;'
'MYSQL_AUDIT_CONNECTION_CONNECT;;';
为了提高可读性,该语句利用了SQL语法,该语法将相邻的字符串连接为单个字符串。
将null_audit_event_order_check
变量设置为事件字符串列表之后,下一个匹配操作将变量值替换为指示操作结果的值:
-
如果成功匹配了预期的事件顺序,则结果
null_audit_event_order_check
值为EVENT-ORDER-OK
。 -
如果
null_audit_event_order_check
指定的值中止匹配的事件(如稍后所述),则结果null_audit_event_order_check
值为EVENT-ORDER-ABORT
。 -
如果预期事件顺序因意外数据而失败,则结果
null_audit_event_order_check
值为EVENT-ORDER-INVALID-DATA
。例如,如果将事件指定为预期影响表t1
但实际受影响,则会发生这种情况t2
。
将null_audit_event_order_check
事件分配给要匹配的事件列表时,应使用event_data
事件字符串的非空部分指定一些事件。下表显示了event_data
这些事件的格式。如果事件使用多个数据值,则必须按照显示的顺序指定它们。可替代地,可以指定一个event_data
值<IGNORE>
来忽略事件数据的内容。在这种情况下,事件是否具有数据并不重要。
适用事件 | 事件数据格式 |
---|---|
MYSQL_AUDIT_COMMAND_START``MYSQL_AUDIT_COMMAND_END |
command_id="id_value" |
MYSQL_AUDIT_GLOBAL_VARIABLE_GET``MYSQL_AUDIT_GLOBAL_VARIABLE_SET |
name="var_value" value="var_value" |
MYSQL_AUDIT_QUERY_NESTED_START``MYSQL_AUDIT_QUERY_NESTED_STATUS_END``MYSQL_AUDIT_QUERY_START``MYSQL_AUDIT_QUERY_STATUS_END |
sql_command_id="id_value" |
MYSQL_AUDIT_TABLE_ACCESS_DELETE``MYSQL_AUDIT_TABLE_ACCESS_INSERT``MYSQL_AUDIT_TABLE_ACCESS_READ``MYSQL_AUDIT_TABLE_ACCESS_UPDATE |
db="db_name" table="table_name" |
在该null_audit_event_order_check
值中,通过ABORT_RET
在command
事件字符串的一部分中指定,可以中止对指定事件的审核API调用。(假定该事件是可以中止的事件。那些先前无法描述的事件。)例如,如前所示,这是插入以下内容的预期事件顺序t1
:
MYSQL_AUDIT_COMMAND_START;command_id="3";
MYSQL_AUDIT_PARSE_PREPARSE;;
MYSQL_AUDIT_PARSE_POSTPARSE;;
MYSQL_AUDIT_GENERAL_LOG;;
MYSQL_AUDIT_QUERY_START;sql_command_id="5";
MYSQL_AUDIT_TABLE_ACCESS_INSERT;db="db1" table="t1";
MYSQL_AUDIT_QUERY_STATUS_END;sql_command_id="5";
MYSQL_AUDIT_GENERAL_RESULT;;
MYSQL_AUDIT_GENERAL_STATUS;;
MYSQL_AUDIT_COMMAND_END;command_id="3";
如何截断执行?
要INSERT
在MYSQL_AUDIT_QUERY_STATUS_END
事件发生时中止语句执行,请这样设置null_audit_event_order_check
(切记在相邻事件字符串之间添加分号分隔符):
SET @@null_audit_event_order_check =
'MYSQL_AUDIT_COMMAND_START;command_id="3";;'
'MYSQL_AUDIT_PARSE_PREPARSE;;;'
'MYSQL_AUDIT_PARSE_POSTPARSE;;;'
'MYSQL_AUDIT_GENERAL_LOG;;;'
'MYSQL_AUDIT_QUERY_START;sql_command_id="5";;'
'MYSQL_AUDIT_TABLE_ACCESS_INSERT;db="db1" table="t1";;'
'MYSQL_AUDIT_QUERY_STATUS_END;sql_command_id="5";ABORT_RET';
不必列出预期在包含command
值的事件字符串之后发生的事件ABORT_RET
。
审计插件与前面的序列匹配后,它将中止事件处理并将错误消息发送给客户端。它还设置null_audit_event_order_check
为EVENT-ORDER-ABORT
:
mysql> INSERT INTO db1.t1 VALUES ('some data');
ERROR 3164 (HY000): Aborted by Audit API ('MYSQL_AUDIT_QUERY_STATUS_END';1).
mysql> SELECT @@null_audit_event_order_check;
+--------------------------------+
| @@null_audit_event_order_check |
+--------------------------------+
| EVENT-ORDER-ABORT |
+--------------------------------+
从审核API通知例程返回非零值是中止事件执行的标准方法。通过将null_audit_abort_value
变量设置为通知例程应返回的值,也可以指定自定义错误代码:
SET @@null_audit_abort_value = 123;
中止序列会产生带有自定义错误代码的标准消息。假设您将这样的审核日志系统变量设置为在匹配SELECT 1
语句发生的事件时中止:
SET @@null_audit_abort_value = 123;
SET @@null_audit_event_order_check =
'MYSQL_AUDIT_COMMAND_START;command_id="3";;'
'MYSQL_AUDIT_PARSE_PREPARSE;;;'
'MYSQL_AUDIT_PARSE_POSTPARSE;;;'
'MYSQL_AUDIT_GENERAL_LOG;;;'
'MYSQL_AUDIT_QUERY_START;sql_command_id="0";ABORT_RET';
然后SELECT 1
在包含自定义错误代码的错误消息中执行结果:
mysql> SELECT 1;
ERROR 3164 (HY000): Aborted by Audit API ('MYSQL_AUDIT_QUERY_START';123).
mysql> SELECT @@null_audit_event_order_check;
+--------------------------------+
| @@null_audit_event_order_check |
+--------------------------------+
| EVENT-ORDER-ABORT |
+--------------------------------+
也可以通过设置null_audit_abort_message
变量指定的自定义消息中止事件。假设您这样设置审核日志系统变量:
SET @@null_audit_abort_message = 'Custom error text.';
SET @@null_audit_event_order_check =
'MYSQL_AUDIT_COMMAND_START;command_id="3";;'
'MYSQL_AUDIT_PARSE_PREPARSE;;;'
'MYSQL_AUDIT_PARSE_POSTPARSE;;;'
'MYSQL_AUDIT_GENERAL_LOG;;;'
'MYSQL_AUDIT_QUERY_START;sql_command_id="0";ABORT_RET';
然后中止序列会导致以下错误消息:
mysql> SELECT 1;
ERROR 3164 (HY000): Custom error text.
mysql> SELECT @@null_audit_event_order_check;
+--------------------------------+
| @@null_audit_event_order_check |
+--------------------------------+
| EVENT-ORDER-ABORT |
+--------------------------------+
要NULL_AUDIT
在测试插件后禁用它,请使用以下语句将其卸载:
UNINSTALL PLUGIN NULL_AUDIT;