扩展 MySQL 8.4  /  ...  /  在身份验证插件中实现代理用户支持

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

可插拔身份验证实现的功能之一是代理用户(请参阅 代理用户)。要使服务器端身份验证插件参与代理用户支持,必须满足以下条件:

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

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

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

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

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

插件开发者确定身份验证字符串的语法规则,并根据这些规则实现插件。假设一个插件采用以逗号分隔的对列表,这些对将外部用户映射到 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'

当服务器调用插件对客户端进行身份验证时,它会将适当的身份验证字符串传递给插件。插件负责:

  1. 将字符串解析为其组件,以确定要使用的映射

  2. 将客户端用户名与映射进行比较

  3. 返回正确的 MySQL 用户名

例如,如果 extuser2example.com 主机连接,服务器将 'extuser1=mysqlusera, extuser2=mysqluserb' 传递给插件,并且插件应该将 mysqluserb 复制到 authenticated_as 中,并以终止空字节结尾。如果 extuser2example.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_proxy AS 'proxied_user';

此外,为代理用户创建一个帐户,并为其授予 plugin_user2PROXY 权限:

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 身份连接:

$> 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 身份连接:

$> 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'