MySQL 5.5+ 引入 Audit Plugin API,允許開發者攔截連線建立/斷開、查詢執行、結果返回、錯誤發生等核心事件,實現合規審計、安全防護、行為分析。本文從原始碼結構、事件結構體、介面函式、編譯部署、生產級最佳實踐五個維度,完整呈現 Audit 插件開發全貌。
一、核心原始碼結構
| 檔案 | 用途 |
sql/sql_audit.h | 公共 API 定義:事件類別、事件結構體、介面版本、描述符結構 |
sql/sql_audit.cc | 核心調度邏輯:mysql_audit_general_log()、mysql_audit_connection_notify() |
plugin/audit_null/audit_null.c | 官方範例插件:最小化實現,適合作為開發骨架 |
二、事件分類與結構體詳細定義
1. 連線類事件
類別常數:MYSQL_AUDIT_CONNECTION_CLASS
| 子類常數 | 值 | 觸發時機 | 結構體 |
MYSQL_AUDIT_CONNECTION_CONNECT | 0 | 認證完成、連線建立 | mysql_event_connection |
MYSQL_AUDIT_CONNECTION_DISCONNECT | 1 | 連線斷開 (正常/異常) |
MYSQL_AUDIT_CONNECTION_CHANGE_USER | 2 | COM_CHANGE_USER 執行後 |
struct mysql_event_connection {
unsigned int event_subclass; // 子類別 (上表)
int status; // 0=成功, 非0=錯誤碼
unsigned long thread_id; // 線程 ID
const char *user; // 客戶端用戶名
unsigned int user_length;
const char *priv_user; // 認證後實際用戶
unsigned int priv_user_length;
const char *external_user; // 外部認證用戶 (PAM/LDAP)
unsigned int external_user_length;
const char *proxy_user; // 代理用戶
unsigned int proxy_user_length;
const char *host; // 客戶端主機名
unsigned int host_length;
const char *ip; // 客戶端 IP
unsigned int ip_length;
const char *database; // 預設資料庫
unsigned int database_length;
};
2. 一般操作類事件
類別常數:MYSQL_AUDIT_GENERAL_CLASS
| 子類常數 | 值 | 觸發時機 | 結構體 |
MYSQL_AUDIT_GENERAL_LOG | 0 | 寫入 General Log 前 | mysql_event_general |
MYSQL_AUDIT_GENERAL_ERROR | 1 | 發送錯誤給客戶端前 |
MYSQL_AUDIT_GENERAL_RESULT | 2 | 結果集發送給客戶端後 |
MYSQL_AUDIT_GENERAL_STATUS | 3 | 發送結果集或錯誤後 (統一收尾) |
struct mysql_event_general {
unsigned int event_subclass;
int general_error_code; // 錯誤碼 (ERROR 子類有效)
unsigned long general_thread_id;
const char *general_user; // 執行用戶
unsigned int general_user_length;
const char *general_command; // 命令類型 (Query, Execute, Prepare...)
unsigned int general_command_length;
const char *general_query; // SQL 文本 (可能為 NULL)
unsigned int general_query_length;
struct charset_info_st *general_charset;
unsigned long long general_time; // 執行耗時 (微秒)
unsigned long long general_rows; // 影響/返回行數
};
三、插件介面函式完整實現
#include
#include
#include
static FILE *audit_log_fp = NULL;
static unsigned long long call_count = 0;
/* 1. 插件初始化 */
static int audit_demo_init(void *arg) {
audit_log_fp = fopen("/var/log/mysql/audit.log", "a");
if (!audit_log_fp) return 1;
return 0;
}
/* 2. 事件通知核心函式 */
static void audit_demo_notify(MYSQL_THD thd, unsigned int event_class, const void *event) {
if (!audit_log_fp) return;
call_count++;
const struct mysql_event_general *gen = (const struct mysql_event_general *) event;
if (event_class == MYSQL_AUDIT_GENERAL_CLASS) {
// 僅記錄成功執行的非空查詢 (RESULT 子類)
if (gen->event_subclass == MYSQL_AUDIT_GENERAL_RESULT &&
gen->general_query && *(gen->general_query)) {
time_t now = time(NULL);
fprintf(audit_log_fp, "%s user=%s query=%s rows=%llu time_us=%llu\n",
ctime(&now), gen->general_user, gen->general_query,
gen->general_rows, gen->general_time);
fflush(audit_log_fp);
}
}
else if (event_class == MYSQL_AUDIT_CONNECTION_CLASS) {
const struct mysql_event_connection *conn = (const struct mysql_event_connection *) event;
if (conn->event_subclass == MYSQL_AUDIT_CONNECTION_CONNECT) {
time_t now = time(NULL);
fprintf(audit_log_fp, "%s CONNECT user=%s host=%s ip=%s db=%s\n",
ctime(&now), conn->user, conn->host, conn->ip, conn->database);
fflush(audit_log_fp);
}
}
}
/* 3. 釋放 THD 相關資源 (可選) */
static void audit_demo_release_thd(MYSQL_THD thd) {
// 若 notify 中分配了記憶體,此處釋放
}
/* 4. 插件卸載清理 */
static int audit_demo_deinit(void *arg) {
if (audit_log_fp) {
fclose(audit_log_fp);
audit_log_fp = NULL;
}
return 0;
}
/* 5. 狀態變數 (SHOW STATUS) */
static struct st_mysql_show_var audit_demo_status[] = {
{"audit_demo_calls", (char *)&call_count, SHOW_ULONGLONG, SHOW_SCOPE_GLOBAL},
{0,0,0,0}
};
/* 6. 插件描述符 */
struct st_mysql_audit audit_demo_descriptor = {
MYSQL_AUDIT_INTERFACE_VERSION, // 介面版本
audit_demo_release_thd, // release_thd
audit_demo_notify, // event_notify
{ (unsigned long) MYSQL_AUDIT_GENERAL_CLASS | (unsigned long) MYSQL_AUDIT_CONNECTION_CLASS } // class_mask
};
/* 7. 插件庫描述符 */
mysql_declare_plugin(audit_demo)
{
MYSQL_AUDIT_PLUGIN,
&audit_demo_descriptor,
"AUDIT_DEMO", // 安裝名稱: INSTALL PLUGIN AUDIT_DEMO SONAME 'audit_demo.so'
"Your Name",
"Demo Audit Plugin",
PLUGIN_LICENSE_GPL,
audit_demo_init,
audit_demo_deinit,
0x0100, // 版本 1.0
audit_demo_status, // 狀態變數
NULL, NULL, 0
}
mysql_declare_plugin_end;
四、編譯與部署步驟
- 取得 MySQL 原始碼:
apt source mysql-server 或官網下載對應版本
- 編譯插件:
cd mysql-source/plugin/audit_null
# 修改 audit_null.c → audit_demo.c
gcc -shared -fPIC -I../../include -I../../sql -o audit_demo.so audit_demo.c
- 安裝至插件目錄:
cp audit_demo.so /usr/local/mysql/lib/plugin/
chown mysql:mysql /usr/local/mysql/lib/plugin/audit_demo.so
- MySQL 內安裝:
INSTALL PLUGIN AUDIT_DEMO SONAME 'audit_demo.so';
SHOW PLUGINS; -- 確認 ACTIVE
- 配置自動載入 (my.cnf):
[mysqld]
plugin-load-add=AUDIT_DEMO=audit_demo.so
audit_demo=FORCE_PLUS_PERMANENT -- 防止誤卸載
五、生產級最佳實踐
| 維度 | 建議 |
| 效能 | 僅記錄必要事件 (如 DDL、DML、失敗登入);避免在 notify 內做複雜解析、網路 I/O、鎖操作 |
| 非同步寫入 | 使用 libuv 或自建無鎖佇列 + 專用寫入線程,避免阻塞 SQL 執行緒 |
| 日誌輪轉 | 配合 logrotate,按日/大小切割,保留 90 天 |
| 結構化輸出 | 輸出 JSON Lines 格式,直接接入 ELK/Loki/ClickHouse 分析 |
| 權限最小化 | 插件以 mysql 系統用戶運行,日誌目錄 chown mysql:mysql、chmod 750 |
| 審計完整性 | 關鍵操作 (GRANT、DROP、TRUNCATE) 強制記錄;配合 class_mask 精準過濾 |
| 相容性測試 | 每次 MySQL 小版本升級後,必須在測試環境驗證插件載入、事件觸發、日誌輸出正常 |
六、現有成熟方案對比
| 方案 | 優點 | 缺點 | 適用場景 |
官方 audit_log (Enterprise) | 原生、高效、JSON/舊版 XML 格式、過濾規則豐富 | 需商業授權 | 企業合規、審計強制要求 |
| MariaDB Audit Plugin | 開源、支援 MariaDB/MySQL、配置靈活、Syslog/檔案雙輸出 | 需替換 Server 或動態載入相容版本 | 開源環境、合規預算有限 |
| McAfee MySQL Audit / Percona Audit | 社區維護、功能完整 | 更新頻率不一 | 特定合規需求 |
| 自研 (本文範例) | 完全可控、業務深度整合、零授權成本 | 開發維護成本高、需自行保證相容性 | 特殊業務邏輯審計、學習研究 |
Audit 插件是 MySQL 可擴展性的典範:核心內核保持精簡,業務邏輯下沉插件層。掌握事件結構、介面契約、部署流程,您即可根據合規需求、安全策略、分析目標,打造專屬的資料庫審計體系。下一篇我們將探討 Amoeba MySQL Proxy 配置文件深度解析與分片規則實戰,敬請期待。