编写身份验证插件
MySQL支持可插入身份验证,其中调用插件来对客户端连接进行身份验证。身份验证插件允许使用身份验证方法,而不是使用存储在mysql.user
系统表中的内置密码方法。例如,可以编写插件来访问外部身份验证方法。同样,身份验证插件可以支持代理用户功能,以便连接用户是另一个用户的代理,并且出于访问控制的目的,将其视为具有其他用户的特权。有关更多信息,请和“代理用户”。
可以为服务器端或客户端编写身份验证插件。服务器端插件使用与其他服务器插件类型(例如,全文分析器或审核插件)相同的插件API(尽管具有不同的类型特定描述符)。客户端插件使用客户端插件API。
几个头文件包含与身份验证插件有关的信息:
plugin.h
:定义MYSQL_AUTHENTICATION_PLUGIN
服务器插件类型。client_plugin.h
:定义客户端插件的API。这包括客户端插件C API调用的客户端插件描述符和函数原型(请参见“ C API客户端插件函数”)。plugin_auth.h
:定义服务器插件API特定于身份验证插件的部分。这包括服务器端身份验证插件的特定于类型的描述符和MYSQL_SERVER_AUTH_INFO
结构。plugin_auth_common.h
:包含客户端和服务器身份验证插件的常见元素。这包括返回值定义和MYSQL_PLUGIN_VIO
结构。
要编写身份验证插件,请在插件源文件中包含以下头文件。根据插件的功能和要求,可能还需要其他MySQL或常规头文件。
对于实现服务器身份验证插件的源文件,请包含以下文件:
#include <mysql/plugin_auth.h>
对于实现客户端身份验证插件(或客户端和服务器插件)的源文件,请包括以下文件:
#include <mysql/plugin_auth.h> #include <mysql/client_plugin.h> #include <mysql.h>
plugin_auth.h
包括plugin.h
和plugin_auth_common.h
,因此您无需显式包括后面的文件。
本节介绍如何编写可一起使用的一对简单服务器和客户端身份验证插件。
警告这些插件接受任何非空密码,并且密码以明文形式发送。这是不安全的,因此不应在生产环境中使用插件。
此处开发的服务器端和客户端插件都命名为auth_simple
。如“插件数据结构”中所述,插件库文件必须与客户端插件具有相同的基本名称,因此源文件名是auth_simple.c
并产生一个名为的库auth_simple.so
(假设您的系统将其.so
用作后缀)。库文件)。
在MySQL源代码发行版中,身份验证插件源位于plugin/auth
目录中,可以作为编写其他身份验证插件的指南进行检查。另外,要了解内置身份验证插件的实现方式,请参阅sql/sql_acl.cc
MySQL服务器sql-common/client.c
中内置的插件和libmysqlclient
客户端库中内置的插件。(对于内置的客户端插件,请注意,auth_plugin_t
此处使用的结构与通常的客户端插件声明宏所使用的结构不同。尤其是,前两个成员是显式提供的,而不是声明宏。)
编写服务器端身份验证插件
用用于所有服务器插件类型的常规通用描述符格式声明服务器端插件(请参见“服务器插件库和插件描述符”)。对于auth_simple
插件,描述符如下所示:
mysql_declare_plugin(auth_simple) { MYSQL_AUTHENTICATION_PLUGIN, &auth_simple_handler, /* type-specific descriptor */ "auth_simple", /* plugin name */ "Author Name", /* author */ "Any-password authentication plugin", /* description */ PLUGIN_LICENSE_GPL, /* license type */ NULL, /* no init function */ NULL, /* no deinit function */ 0x0100, /* version = 1.0 */ NULL, /* no status variables */ NULL, /* no system variables */ NULL, /* no reserved information */ 0 /* no flags */ } mysql_declare_plugin_end;
所述name
构件(auth_simple
)指示要用于如在语句中的插件的引用名称INSTALL PLUGIN
或UNINSTALL PLUGIN
。这也是由SHOW PLUGINS
或显示的名称INFORMATION_SCHEMA.PLUGINS
。
auth_simple_handler
通用描述符的成员指向特定于类型的描述符。对于身份验证插件,特定于类型的描述符是st_mysql_auth
结构的实例(在中定义plugin_auth.h
):
struct st_mysql_auth {int interface_version;const char *client_auth_plugin;int (*authenticate_user)(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info);int (*generate_authentication_string)(char *outbuf,unsigned int *outbuflen,const char *inbuf,unsigned int inbuflen);int (*validate_authentication_string)(char *const inbuf,unsigned int buflen);int (*set_salt)(const char *password,unsigned int password_len,unsigned char * salt,unsigned char *salt_len);const unsigned long authentication_flags; };
该st_mysql_auth
结构包含以下成员:
interface_version
:始终是特定于类型的API版本号MYSQL_AUTHENTICATION_INTERFACE_VERSION
client_auth_plugin
:客户端插件名称authenticate_user
:指向与客户端通信的主插件函数的指针generate_authentication_string
:指向插件函数的指针,该函数从身份验证字符串生成密码摘要validate_authentication_string
:指向用于验证密码摘要的插件功能的指针set_salt
:指向插件函数的指针,该函数将加密的密码转换为二进制形式authentication_flags
:标志词
client_auth_plugin
如果需要特定的插件,成员应指出客户端插件的名称。值NULL
表示“任何插件。”在后一种情况下,任何插件的客户端使用就行了。如果服务器插件不关心客户端插件或其发送的用户名或密码,这将很有用。例如,如果服务器插件仅对本地客户端进行身份验证并使用操作系统的某些属性,而不使用客户端插件发送的信息,则可能为true。
对于auth_simple
,类型特定的描述符如下所示:
static struct st_mysql_auth auth_simple_handler = { MYSQL_AUTHENTICATION_INTERFACE_VERSION, "auth_simple", /* required client-side plugin name */ auth_simple_server /* server-side plugin main function */ generate_auth_string_hash, /* generate digest from password string */ validate_auth_string_hash, /* validate password digest */ set_salt, /* generate password salt value */ AUTH_FLAG_PRIVILEGED_USER_FOR_PASSWORD_CHANGE };
主要功能auth_simple_server()
接受两个参数,分别代表一个I / O结构和一个MYSQL_SERVER_AUTH_INFO
结构。在中找到的结构定义plugin_auth.h
如下所示:
typedef struct st_mysql_server_auth_info {char *user_name;unsigned int user_name_length;const char *auth_string;unsigned long auth_string_length;char authenticated_as[MYSQL_USERNAME_LENGTH+1];char external_user[512];int password_used;const char *host_or_ip;unsigned int host_or_ip_length; } MYSQL_SERVER_AUTH_INFO;
字符串成员的字符集为UTF-8。如果存在_length
与字符串关联的成员,则以字节为单位指示字符串长度。字符串也以空值结尾。
服务器调用身份验证插件时,它应按以下方式解释MYSQL_SERVER_AUTH_INFO
结构成员。如图所示,其中一些用于设置客户端会话中的SQL函数或系统变量的值。
user_name
:客户端发送的用户名。该值成为USER()
功能值。user_name_length
:的长度(user_name
以字节为单位)。auth_string
:该值authentication_string
的行的列mysql.user
的匹配的帐户名系统表(即,相匹配的客户机的用户名和主机名和该服务器使用,以确定如何验证客户端的行)。假设您使用以下语句创建一个帐户:
CREATE USER 'my_user'@'localhost'IDENTIFIED WITH my_pluginAS 'my_auth_string';当
my_user
从本地主机,服务器调用所连接my_plugin
并穿过作为它的值。'my_auth_string'
auth_string
auth_string_length
:的长度(auth_string
以字节为单位)。authenticated_as
:服务器将其设置为用户名(的值user_name
)。插件可以对其进行更改,以指示客户端应具有其他用户的特权。例如,如果插件支持代理用户,则初始值为连接(代理)用户的名称,并且插件可以将此成员更改为代理用户名。然后,服务器将代理用户视为具有代理用户的特权(假定满足其他对代理用户支持的条件;请参见“在身份验证插件中实现代理用户支持”))。该值表示为MYSQL_USER_NAME_LENGTH
最长为字节的字符串,再加上终止的null。该值成为CURRENT_USER()
功能值。external_user
:服务器将其设置为空字符串(终止为空)。其值成为external_user
系统变量值。如果插件希望该系统变量具有不同的值,则应相应地设置此成员(例如,设置为连接的用户名)。该值表示为最多511个字节长的字符串,再加上一个终止null。password_used
:身份验证失败时,该成员适用。插件可以设置它或忽略它。该值用于构造的失败错误消息Authentication fails. Password used:%s
。值password_used
决定如何%s
处理,如下表所示。password_used
%s
处理方式0 没有 1个 是 2 将没有 %s
host_or_ip
:客户端主机的名称(如果可以解析),否则为IP地址。host_or_ip_length
:的长度(host_or_ip
以字节为单位)。
的auth_simple
主要功能,auth_simple_server()
,读取来自客户端的密码(一个空终止字符串)和成功,如果密码不为空(第一个字节不是null):
static int auth_simple_server (MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info) {unsigned char *pkt;int pkt_len; /* read the password as null-terminated string, fail on error */if ((pkt_len= vio->read_packet(vio, &pkt)) < 0)return CR_ERROR; /* fail on empty password */if (!pkt_len || *pkt == '\0') { info->password_used= PASSWORD_USED_NO;return CR_ERROR; } /* accept any nonempty password */ info->password_used= PASSWORD_USED_YES;return CR_OK; }
主要功能应返回下表所示的错误代码之一。
错误代码 | 含义 |
---|---|
CR_OK | 成功 |
CR_OK_HANDSHAKE_COMPLETE | 不要将状态数据包发送回客户端 |
CR_ERROR | 错误 |
CR_AUTH_USER_CREDENTIALS | 验证失败 |
CR_AUTH_HANDSHAKE | 验证握手失败 |
CR_AUTH_PLUGIN_ERROR | 内部插件错误 |
有关握手如何工作的示例,请参见plugin/auth/dialog.c
源文件。
服务器在“性能模式”host_cache
表中计算插件错误。
auth_simple_server()
密码如此基本,以至于除了设置指示是否收到密码的成员外,它不使用身份验证信息结构。
支持代理用户的插件必须将代理用户的名称(客户端用户应获得其特权的MySQL用户)返回服务器。为此,插件必须将info->authenticated_as
成员设置为代理用户名。有关代理的信息,请参见“代理用户”和“在身份验证插件中实现代理用户支持”。
generate_authentication_string
插件描述符的成员获取密码并从中生成密码哈希(摘要):
- 前两个参数是指向输出缓冲区及其最大长度(以字节为单位)的指针。该函数应将密码哈希写入输出缓冲区,并将长度重置为实际哈希长度。
- 后两个参数指示密码输入缓冲区及其长度(以字节为单位)。
- 该函数返回0表示成功,如果发生错误则返回1。
对于auth_simple
插件,该generate_auth_string_hash()
函数实现generate_authentication_string
成员。它只是复制密码,除非密码太长而无法容纳在输出缓冲区中。
int generate_auth_string_hash(char *outbuf,unsigned int *buflen,const char *inbuf,unsigned int inbuflen) { /* fail if buffer specified by server cannot be copied to output buffer */if (*buflen < inbuflen)return 1; /* error */ strncpy(outbuf, inbuf, inbuflen); *buflen= strlen(inbuf);return 0; /* success */ }
validate_authentication_string
插件描述符的成员验证密码哈希:
- 参数是指向密码哈希及其长度(以字节为单位)的指针。
- 该函数成功返回0,如果无法验证密码哈希,则返回1。
对于auth_simple
插件,该validate_auth_string_hash()
函数实现validate_authentication_string
成员。它无条件地返回成功:
int validate_auth_string_hash(char *const inbuf __attribute__((unused)),unsigned int buflen __attribute__((unused))) {return 0; /* success */ }
set_salt
插件描述符的成员仅由mysql_native_password
插件使用(请参见“本地可插入身份验证”)。对于其他身份验证插件,可以使用以下简单实现:
int set_salt(const char * password __attribute__((unused)),unsigned int password_len __attribute__((unused)),unsigned char * salt __attribute__((unused)),unsigned char * salt_len) { *salt_len= 0;return 0; /* success */ }
authentication_flags
插件描述符的成员包含影响插件操作的标志。允许的标志是:
AUTH_FLAG_PRIVILEGED_USER_FOR_PASSWORD_CHANGE
:凭据更改是特权操作。如果设置了此标志,则服务器要求用户具有全局CREATE USER
特权或数据库UPDATE
特权mysql
。AUTH_FLAG_USES_INTERNAL_STORAGE
:无论插件使用内部存储(在authentication_string
列mysql.user
行)。如果未设置此标志,则尝试设置密码的尝试将失败,服务器将发出警告。
编写客户端身份验证插件
使用mysql_declare_client_plugin()
和mysql_end_client_plugin
宏声明客户端插件描述符(请参见“插件数据结构”)。对于auth_simple
插件,描述符如下所示:
mysql_declare_client_plugin(AUTHENTICATION) "auth_simple", /* plugin name */ "Author Name", /* author */ "Any-password authentication plugin", /* description */ {1,0,0}, /* version = 1.0.0 */ "GPL", /* license type */ NULL, /* for internal use */ NULL, /* no init function */ NULL, /* no deinit function */ NULL, /* no option-handling function */ auth_simple_client /* main function */ mysql_end_client_plugin;
从插件名称到选项处理功能的描述符成员对于所有客户端插件类型都是通用的。(有关描述,请参见“插件数据结构”。)在公共成员之后,该描述符还有一个特定于身份验证插件的成员。这是“主要”功能,用于处理与服务器的通信。该函数接受两个参数,分别代表I / O结构和连接处理程序。对于我们的简单的无密码插件,main函数除了将用户提供的密码写入服务器外,什么也没有做:
static int auth_simple_client (MYSQL_PLUGIN_VIO *vio, MYSQL *mysql) {int res; /* send password as null-terminated string as cleartext */ res= vio->write_packet(vio, (const unsigned char *) mysql->passwd, strlen(mysql->passwd) + 1);return res ? CR_ERROR : CR_OK; }
主要功能应返回下表所示的错误代码之一。
错误代码 | 含义 |
---|---|
CR_OK | 成功 |
CR_OK_HANDSHAKE_COMPLETE | 成功,客户完成 |
CR_ERROR | 错误 |
CR_OK_HANDSHAKE_COMPLETE
表示客户端已成功完成其部分并已读取最后一个数据包。CR_OK_HANDSHAKE_COMPLETE
如果身份验证协议中的往返次数事先未知,则客户端插件可能会返回,并且该插件必须读取另一个数据包以确定身份验证是否完成。
使用身份验证插件
要编译和安装插件库文件,请使用“编译和安装插件库”中的说明。要使该库文件可供使用,请将其安装在插件目录(由plugin_dir
系统变量命名的目录)中。
在服务器上注册服务器端插件。例如,要在服务器启动时加载插件,请使用一个--plugin-load=auth_simple.so
选项(.so
根据需要调整平台的后缀)。
创建一个用户,服务器将使用该用户的auth_simple
插件进行身份验证:
mysql>CREATE USER 'x'@'localhost' ->IDENTIFIED WITH auth_simple;
使用客户端程序以用户身份连接到服务器x
。服务器端auth_simple
插件与客户端程序通信,它应使用客户端auth_simple
插件,后者将密码发送到服务器。服务器插件应拒绝发送空密码的连接,并接受发送非空密码的连接。每种方式调用客户端程序以验证这一点:
shell>mysql --user=x --skip-password ERROR 1045 (28000): Access denied for user 'x'@'localhost' (using password: NO) shell>mysql --user=x --password Enter password: abc mysql>
由于服务器插件接受任何非空密码,因此应将其视为不安全的密码。在测试插件以确认其正常工作之后,请在不使用该--plugin-load
选项的情况下重新启动服务器,以免使服务器始终在运行时加载不安全的身份验证插件。另外,使用删除用户DROP USER 'x'@'localhost'
。
有关加载和使用身份验证插件的更多信息,请参见“MySQL服务器插件”和“可插入身份验证”。
如果要编写支持身份验证插件使用的客户端程序,则通常该程序会通过调用mysql_options()
设置MYSQL_DEFAULT_AUTH
和MYSQL_PLUGIN_DIR
选项来导致加载插件:
char *plugin_dir = "path_to_plugin_dir";char *default_auth = "plugin_name"; /* ... process command-line options ... */ mysql_options(&mysql, MYSQL_PLUGIN_DIR, plugin_dir); mysql_options(&mysql, MYSQL_DEFAULT_AUTH, default_auth);
通常,该程序还将接受--plugin-dir
和--default-auth
选项,使用户能够覆盖默认值。
如果客户端程序需要较低级别的插件管理,则客户端库应包含带有st_mysql_client_plugin
参数的函数。请参见“ C API客户端插件功能”。
在身份验证插件中实现代理用户支持
可插拔身份验证使其中一种功能成为代理用户(请参见“代理用户”)。为了使服务器端身份验证插件能够参与代理用户支持,必须满足以下条件:
- 当将连接的客户端视为代理用户时,插件必须
authenticated_as
在MYSQL_SERVER_AUTH_INFO
结构的成员中返回不同的名称,以指示代理的用户名。它还可以选择设置external_user
成员,以设置external_user
系统变量的值。 - 代理用户帐户必须设置为由插件进行身份验证。使用
CREATE USER
orGRANT
语句将帐户与插件关联。 - 代理用户帐户必须具有
PROXY
代理帐户的特权。使用该GRANT
语句授予此特权。
换句话说,插件所需的代理用户支持的唯一方面是将其设置authenticated_as
为代理用户名。其余的是可选的(设置external_user
)或由DBA使用SQL语句完成。
身份验证插件如何确定代理用户连接时返回哪个代理用户?这取决于插件。通常,该插件根据服务器传递给它的身份验证字符串将客户端映射到代理用户。此字符串来自于AS
该部分IDENTIFIED WITH
的条款CREATE USER
,指定使用的插件进行身份验证声明。
插件开发人员确定身份验证字符串的语法规则,并根据这些规则实现插件。假设一个插件采用逗号分隔的成对列表,这些对将外部用户映射到MySQL用户。例如:
CREATE USER ''@'%.example.com'IDENTIFIED WITH my_pluginAS 'extuser1=mysqlusera, extuser2=mysqluserb'CREATE USER ''@'%.example.org'IDENTIFIED WITH my_pluginAS 'extuser1=mysqluserc, extuser2=mysqluserd'
服务器调用插件对客户端进行身份验证时,会将适当的身份验证字符串传递给该插件。该插件负责:
- 将字符串解析为其组件,以确定要使用的映射
- 将客户端用户名与映射进行比较
- 返回正确的MySQL用户名
例如,如果extuser2
从example.com
主机进行连接,则服务器将传递'extuser1=mysqlusera, extuser2=mysqluserb'
给插件,并且该插件应复制mysqluserb
到authenticated_as
,并带有一个终止的空字节。如果extuser2
从example.org
主机连接,则服务器通过'extuser1=mysqluserc, extuser2=mysqluserd'
,而插件应mysqluserd
改为复制。
如果映射中没有匹配项,则操作取决于插件。如果需要匹配,则插件可能会返回错误。否则,插件可能只返回客户端名称;在这种情况下,它不应更改authenticated_as
,并且服务器不会将客户端视为代理。
以下示例演示了如何使用名为的插件处理代理用户auth_simple_proxy
。像auth_simple
前面描述的插件一样,auth_simple_proxy
接受任何非空密码都是有效的(因此,不应在生产环境中使用)。此外,它检查auth_string
身份验证字符串成员,并使用以下非常简单的规则对其进行解释:
- 如果字符串为空,则插件将返回给定的用户名,并且不会发生代理。也就是说,该插件的值
authenticated_as
保持不变。 - 如果字符串为非空字符串,则插件会将其视为代理用户的名称并将其复制到,
authenticated_as
以便进行代理。
为了进行测试,请设置一个未根据前述规则进行代理的帐户,然后进行设置。这意味着一个帐户没有AS
子句,而其中一个AS
子句为代理用户命名:
CREATE USER 'plugin_user1'@'localhost'IDENTIFIED WITH auth_simple_proxy;CREATE USER 'plugin_user2'@'localhost'IDENTIFIED WITH auth_simple_proxyAS 'proxied_user';
此外,创建一个由代理用户帐户并授予plugin_user2
的PROXY
特权吧:
CREATE USER 'proxied_user'@'localhost'IDENTIFIED BY 'proxied_user_pass';GRANT PROXY ON 'proxied_user'@'localhost'TO 'plugin_user2'@'localhost';
服务器调用身份验证插件之前,它会设置authenticated_as
为客户端用户名。为了表明用户是代理,插件应设置authenticated_as
为代理用户名。对于auth_simple_proxy
,这意味着它必须检查该auth_string
值,如果该值是非空的,则将其复制到authenticated_as
成员以将其作为代理用户的名称返回。另外,当发生代理时,插件将external_user
成员设置为客户端用户名;这将成为external_user
系统变量的值。
static int auth_simple_proxy_server (MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info) {unsigned char *pkt;int pkt_len; /* read the password as null-terminated string, fail on error */if ((pkt_len= vio->read_packet(vio, &pkt)) < 0)return CR_ERROR; /* fail on empty password */if (!pkt_len || *pkt == '\0') { info->password_used= PASSWORD_USED_NO;return CR_ERROR; } /* accept any nonempty password */ info->password_used= PASSWORD_USED_YES; /* if authentication string is nonempty, use as proxied user name */ /* and use client name as external_user value */if (info->auth_string_length > 0) { strcpy (info->authenticated_as, info->auth_string); strcpy (info->external_user, info->user_name); }return CR_OK; }
成功连接后,该USER()
功能应指示正在连接的客户端用户和主机名,并CURRENT_USER()
应指示在会话期间应用特权的帐户。如果没有发生代理,则后一个值应为连接用户帐户;如果发生代理,则为代理帐户。
编译并安装插件,然后对其进行测试。首先,连接为plugin_user1
:
shell>mysql --user=plugin_user1 --password Enter password: x
在这种情况下,不应有代理:
mysql>SELECT USER(), CURRENT_USER(), @@proxy_user, @@external_user\G *************************** 1. row*************************** USER(): plugin_user1@localhost CURRENT_USER(): plugin_user1@localhost @@proxy_user: NULL @@external_user: NULL
然后连接为plugin_user2
:
shell>mysql --user=plugin_user2 --password Enter password: x
在这种情况下,plugin_user2
应代理proxied_user
:
mysql>SELECT USER(), CURRENT_USER(), @@proxy_user, @@external_user\G *************************** 1. row*************************** USER(): plugin_user2@localhost CURRENT_USER(): proxied_user@localhost @@proxy_user: 'plugin_user2'@'localhost' @@external_user: 'plugin_user2'@'localhost'