28.2.4.9 编写身份验证插件

MySQL 支持可插入身份验证,其中调用插件来身份验证 Client 端连接。身份验证插件可以使用身份验证方法,而不是使用mysql.user系统 table 中存储的内置密码方法。例如,可以编写插件来访问外部身份验证方法。同样,身份验证插件可以支持代理用户功能,以便连接用户是另一个用户的代理,并且出于访问控制的目的,将其视为具有其他用户的特权。有关更多信息,请参见第 6.2.13 节“可插入身份验证”第 6.2.14 节“代理用户”

可以为服务器端或 Client 端编写身份验证插件。服务器端插件使用与其他服务器插件类型(例如,全文解析器或审核插件)相同的插件 API(尽管具有不同的类型特定 Descriptors)。Client 端插件使用 Client 端插件 API。

几个头文件包含与身份验证插件有关的信息:

  • plugin.h:定义MYSQL_AUTHENTICATION_PLUGIN服务器插件类型。

  • client_plugin.h:定义 Client 端插件的 API。这包括 Client 端插件 Descriptors 和 Client 端插件 C API 调用的函数原型(请参见第 27.7.13 节“ C APIClient 端插件功能”)。

  • plugin_auth.h:定义服务器插件 API 特定于身份验证插件的部分。这包括服务器端身份验证插件的特定类型 Descriptors 和MYSQL_SERVER_AUTH_INFO结构。

  • plugin_auth_common.h:包含 Client 端和服务器身份验证插件的常见元素。这包括返回值定义和MYSQL_PLUGIN_VIO结构。

要编写身份验证插件,请在插件源文件中包含以下头文件。根据插件的功能和要求,可能还需要其他 MySQL 或常规头文件。

  • 对于实现服务器身份验证插件的源文件,请包含以下文件:
#include <mysql/plugin_auth.h>
  • 对于实现 Client 端身份验证插件(或 Client 端和服务器插件)的源文件,请包括以下文件:
#include <mysql/plugin_auth.h>
#include <mysql/client_plugin.h>
#include <mysql.h>

plugin_auth.h包含plugin.hplugin_auth_common.h,因此您无需显式包括后者。

本节介绍如何编写可一起使用的一对简单服务器和 Client 端身份验证插件。

Warning

这些插件接受任何非空密码,并且密码以明文形式发送。这是不安全的,因此插件不应在生产环境中使用.

此处开发的服务器端和 Client 端插件都命名为auth_simple。如第 28.2.4.2 节“插件数据结构”中所述,插件库文件必须与 Client 端插件具有相同的基本名称,因此源文件名是auth_simple.c并生成一个名为auth_simple.so的库(假设您的系统使用.so作为库文件的后缀)。

在 MySQL 源代码发行版中,身份验证插件源位于plugin/auth目录中,可以作为编写其他身份验证插件的指南进行检查。另外,要了解内置身份验证插件的实现方式,请参见sql/sql_acl.cc(针对内置于 MySQL 服务器的插件)和sql-common/client.c(针对针对内置于libmysqlclientClient 端库的插件)。 (对于内置 Client 端插件,请注意,此处使用的auth_plugin_t结构与通常的 Client 端插件声明宏所使用的结构不同.特别是,前两个成员是显式提供的,而不是声明宏.)

28.2.4.9.1 编写服务器端身份验证插件

用用于所有服务器插件类型的常规通用 Descriptors 格式声明服务器端插件(请参见第 28.2.4.2.1 节,“服务器插件库和插件 Descriptors”)。对于auth_simple插件,Descriptors 如下所示:

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)table 示在诸如INSTALL PLUGINUNINSTALL PLUGIN之类的语句中用于引用插件的名称。这也是SHOW PLUGINSINFORMATION_SCHEMA.PLUGINS显示的名称。

常规 Descriptors 的auth_simple_handler成员指向特定于类型的 Descriptors。对于身份验证插件,特定于类型的 Descriptors 是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:Client 端插件名称

  • authenticate_user:指向与 Client 端通信的主插件功能的指针

  • generate_authentication_string:指向插件函数的指针,该函数从身份验证字符串生成密码摘要

  • validate_authentication_string:指向用于验证密码摘要的插件功能的指针

  • set_salt:指向将加密密码转换为二进制形式的插件函数的指针

  • authentication_flags:标志词

如果需要特定的插件,client_auth_plugin成员应指出 Client 端插件的名称。值NULLtable 示“任何插件”。在后一种情况下,Client 端使用的任何插件都可以。如果服务器插件不关心 Client 端插件或其发送的用户名或密码,这将很有用。例如,如果服务器插件仅对本地 Client 端进行身份验证并使用 os 的某些属性,而不使用 Client 端插件发送的信息,则可能为 true。

对于auth_simple,类型特定的 Descriptors 如下所示:

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()接受两个参数,分别代 tableI/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结构成员。如图所示,其中一些用于设置 Client 端会话中的 SQL 函数或系统变量的值。

  • user_name:Client 端发送的用户名。该值成为USER()函数值。

  • user_name_lengthuser_name的长度(以字节为单位)。

  • auth_stringmysql.user系统 table 中与帐户名匹配的行(即与 Client 端用户名和主机名匹配的行,服务器用于确定如何验证 Client 端的行)的authentication_string列的值。

假设您使用以下语句创建一个帐户:

CREATE USER 'my_user'@'localhost'
  IDENTIFIED WITH my_plugin AS 'my_auth_string';

my_user从 localhost 连接时,服务器将调用my_plugin并将'my_auth_string'作为auth_string值传递给它。

  • auth_string_lengthauth_string的长度(以字节为单位)。

  • authenticated_as:服务器将其设置为用户名(user_name的值)。该插件可以对其进行更改,以指示 Client 端应具有其他用户的特权。例如,如果插件支持代理用户,则初始值为连接(代理)用户的名称,并且插件可以将此成员更改为代理的用户名。然后,服务器将代理用户视为具有代理用户的特权(假设满足其他支持代理用户的条件;请参阅第 28.2.4.9.4 节“在身份验证插件中实现代理用户支持”)。该值 table 示为最多MYSQL_USER_NAME_LENGTH个字节长的字符串,再加上一个终止 null。该值成为CURRENT_USER()函数值。

  • external_user:服务器将其设置为空字符串(以 null 终止)。其值成为external_user系统变量值。如果插件希望该系统变量具有不同的值,则应相应地设置此成员(例如,设置为连接的用户名)。该值 table 示为最多 511 个字节长的字符串,再加上一个终止 null。

  • password_used:身份验证失败时,此成员适用。该插件可以设置它或忽略它。该值用于构造Authentication fails. Password used: %s的故障错误消息。 password_used的值确定%s的处理方式,如下 table 所示。

password_used%s处理
0NO
1YES
2不会有%s
  • host_or_ip:Client 端主机的名称(如果可以解析),否则为 IP 地址。

  • host_or_ip_lengthhost_or_ip的长度(以字节为单位)。

auth_simple主要函数auth_simple_server()从 Client 端读取密码(以空值终止的字符串),如果密码非空(第一个字节不为空),则成功:

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;
}

主要功能应返回下 table 所示的错误代码之一。

Error CodeMeaning
CR_OKSuccess
CR_OK_HANDSHAKE_COMPLETE不要将状态数据包发送回 Client 端
CR_ERRORError
CR_AUTH_USER_CREDENTIALSAuthentication failure
CR_AUTH_HANDSHAKE验证握手失败
CR_AUTH_PLUGIN_ERROR内部插件错误

有关握手如何工作的示例,请参见plugin/auth/dialog.c源文件。

服务器在“性能模式host_cache”table 中计算插件错误。

auth_simple_server()非常基础,以至于除了设置指示是否接收到密码的成员之外,它不使用身份验证信息结构。

支持代理用户的插件必须将代理用户的名称(Client 端用户应获得其特权的 MySQL 用户)返回服务器。为此,插件必须将info->authenticated_as成员设置为代理用户名。有关代理的信息,请参见第 6.2.14 节“代理用户”第 28.2.4.9.4 节“在身份验证插件中实现代理用户支持”

插件 Descriptors 的generate_authentication_string成员获取密码并从中生成密码哈希(摘要):

  • 前两个参数是指向输出缓冲区及其最大长度(以字节为单位)的指针。该函数应将密码哈希值写入输出缓冲区,并将长度重置为实际哈希值长度。

  • 后两个参数指示密码 Importing 缓冲区及其长度(以字节为单位)。

  • 该函数返回 0table 示成功,如果发生错误则返回 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 */
}

插件 Descriptors 的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 */
}

插件 Descriptors 的set_salt成员仅由mysql_native_password插件使用(请参见第 6.4.1.1 节“本地可插入身份验证”)。对于其他身份验证插件,可以使用以下简单实现:

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 */
}

插件 Descriptors 的authentication_flags成员包含影响插件操作的标志。允许的标志是:

  • AUTH_FLAG_PRIVILEGED_USER_FOR_PASSWORD_CHANGE:凭据更改是特权操作。如果设置了此标志,则服务器要求用户具有mysql数据库的全局CREATE USER特权或UPDATE特权。

  • AUTH_FLAG_USES_INTERNAL_STORAGE:插件是否使用内部存储(在mysql.user行的authentication_string列中)。如果未设置此标志,则尝试设置密码失败,服务器将发出警告。

28.2.4.9.2 编写 Client 端身份验证插件

使用mysql_declare_client_plugin()mysql_end_client_plugin宏声明 Client 端插件 Descriptors(请参见第 28.2.4.2.3 节“Client 端插件 Descriptors”)。对于auth_simple插件,Descriptors 如下所示:

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;

从插件名称到选项处理功能的 Descriptors 成员对于所有 Client 端插件类型都是通用的。 (有关说明,请参见第 28.2.4.2.3 节“Client 端插件 Descriptors”。)在公共成员之后,Descriptors 还有一个特定于身份验证插件的成员。这是“主要”功能,用于处理与服务器的通信。该函数接受两个参数,分别代 tableI/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;
}

主要功能应返回下 table 所示的错误代码之一。

Error CodeMeaning
CR_OKSuccess
CR_OK_HANDSHAKE_COMPLETE成功,Client 完成
CR_ERRORError

CR_OK_HANDSHAKE_COMPLETEtable 示 Client 端已成功完成其部分并已读取最后一个数据包。如果身份验证协议中的往返次数事先未知,则 Client 端插件可能会返回CR_OK_HANDSHAKE_COMPLETE,并且该插件必须读取另一个数据包以确定身份验证是否完成。

28.2.4.9.3 使用身份验证插件

要编译和安装插件库文件,请使用第 28.2.4.3 节“编译和安装插件库”中的说明。要使该库文件可供使用,请将其安装在插件目录(由plugin_dir系统变量命名的目录)中。

在服务器上注册服务器端插件。例如,要在服务器启动时加载插件,请使用--plugin-load=auth_simple.so选项,并根据需要调整平台的.so后缀。

创建服务器将使用auth_simple插件进行身份验证的用户:

mysql> CREATE USER 'x'@'localhost'
    -> IDENTIFIED WITH auth_simple;

使用 Client 端程序以用户x的身份连接到服务器。服务器端auth_simple插件与 Client 端程序通信,它应使用 Client 端auth_simple插件,后者将密码发送给服务器。服务器插件应拒绝发送空密码的连接,并接受发送非空密码的连接。每种方式调用 Client 端程序以验证这一点:

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选项的情况下重新启动服务器,以免使服务器始终在运行时加载不安全的身份验证插件。另外,使用删除用户'x'@'localhost'删除用户。

有关加载和使用身份验证插件的其他信息,请参见第 5.5.1 节“安装和卸载插件”第 6.2.13 节“可插入身份验证”

如果要编写一个支持使用身份验证插件的 Client 端程序,通常这样的程序会通过调用mysql_options()设置MYSQL_DEFAULT_AUTHMYSQL_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选项,这些选项使用户可以覆盖默认值。

如果 Client 端程序需要较低级别的插件 Management,则 Client 端库应包含带有st_mysql_client_plugin参数的函数。参见第 27.7.13 节“ C APIClient 端插件功能”

28.2.4.9.4 在身份验证插件中实现代理用户支持

代理用户(请参见第 6.2.14 节“代理用户”)是可插入身份验证使之成为可能的功能之一。为了使服务器端身份验证插件参与代理用户支持,必须满足以下条件:

  • 当将连接 Client 端视为代理用户时,插件必须在MYSQL_SERVER_AUTH_INFO结构的authenticated_as成员中返回不同的名称,以指示代理的用户名。它还可以选择设置external_user成员,以设置external_user系统变量的值。

  • 代理用户帐户必须设置为由插件进行身份验证。使用CREATE USERGRANT语句将帐户与插件关联。

  • 代理用户帐户必须对代理帐户具有PROXY特权。使用GRANT语句授予此特权。

换句话说,插件所需的代理用户支持的唯一方面是它将authenticated_as设置为代理用户名。其余的是可选的(设置external_user)或由 DBA 使用 SQL 语句完成。

身份验证插件如何确定代理用户连接时返回哪个代理用户?这取决于插件。通常,该插件根据服务器传递给它的身份验证字符串将 Client 端 Map 到代理用户。该字符串来自CREATE USER语句的IDENTIFIED WITH子句的AS部分,该子句指定使用插件进行身份验证。

插件开发人员确定身份验证字符串的语法规则,并根据这些规则实现插件。假设一个插件采用逗号分隔的成对列 table,这些对将外部用户 Map 到 MySQL 用户。例如:

CREATE USER ''@'%.example.com'
  IDENTIFIED WITH my_plugin AS 'extuser1=mysqlusera, extuser2=mysqluserb'
CREATE USER ''@'%.example.org'
  IDENTIFIED WITH my_plugin AS 'extuser1=mysqluserc, extuser2=mysqluserd'

当服务器调用插件来认证 Client 端时,它将适当的认证字符串传递给插件。该插件负责:

  • 将字符串解析为其组件,以确定要使用的 Map

  • 将 Client 端用户名与 Map 进行比较

  • 返回正确的 MySQL 用户名

例如,如果extuser2example.com主机连接,则服务器将'extuser1=mysqlusera, extuser2=mysqluserb'传递给插件,并且插件应将mysqluserb复制到authenticated_as中,并带有一个终止的空字节。如果extuser2example.org主机连接,则服务器将通过'extuser1=mysqluserc, extuser2=mysqluserd',并且插件应改为复制mysqluserd

如果 Map 中没有匹配项,则操作取决于插件。如果需要匹配,则插件可能会返回错误。否则,插件可能只返回 Client 名称;在这种情况下,它不应更改authenticated_as,并且服务器不会将 Client 端视为代理。

以下示例演示了如何使用名为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_proxy AS '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设置为 Client 端用户名。为了 table 明用户是代理,插件应将authenticated_as设置为代理用户名。对于auth_simple_proxy,这意味着它必须检查auth_string的值,如果该值是非空的,则将其复制到authenticated_as成员以将其作为代理用户的名称返回。另外,当发生代理时,该插件将external_user成员设置为 Client 端用户名;这将成为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()函数应指示正在连接的 Client 端用户和主机名,而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'