找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

344

积分

1

好友

36

主题
发表于 4 天前 | 查看: 5| 回复: 0

现象

在实际数据库运维中,一个常见但易混淆的案例是:在新创建的 MySQL 数据库实例中,使用用户 u2 登录时返回了 Plugin 'mysql_native_password' is not loaded 错误。

$ mysql -h127.0.0.1 -P3316 -uu2 -p123
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 1524 (HY000): Plugin 'mysql_native_password' is not loaded

奇怪的是,检查 mysql.user 表后发现:

  • 实例中并没有 u2 这个用户
  • 现有用户中没有任何用户在使用 mysql_native_password 插件
mysql> select host,user,plugin from mysql.user;
+-----------+------------------+-----------------------+
| host      | user             | plugin                |
+-----------+------------------+-----------------------+
| %         | root             | caching_sha2_password |
| localhost | mysql.infoschema | caching_sha2_password |
| localhost | mysql.session    | caching_sha2_password |
| localhost | mysql.sys        | caching_sha2_password |
| localhost | root             | caching_sha2_password |
+-----------+------------------+-----------------------+
5 rows in set (0.05 sec)

更有趣的是,如果使用另一个不存在的用户 u1 登录,返回的错误却是 Access denied for user 'xxx'@'xxx'

$ mysql -h127.0.0.1 -P3316 -uu1 -p123
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 1045 (28000): Access denied for user 'u1'@'127.0.0.1' (using password: YES)

这引发了两个关键问题:

  1. 同样是不存在的用户,为什么 u1 和 u2 会返回不同的错误信息?
  2. 明明没有用户配置 mysql_native_password 插件,为什么 MySQL 会提示该插件未加载?

根因分析

下面结合 MySQL 客户端与服务端的认证流程机制,深入分析上述报错现象。

认证流程概述

一、客户端向 MySQL 服务端发起连接请求。

二、服务端收到请求后,调用 do_auth_once() 函数进行身份认证。首次认证时,MySQL 使用默认密码认证插件。在 MySQL 8.4 之前,默认插件由 default_authentication_plugin 参数决定:

  • 5.7 版本默认是 mysql_native_password
  • 8.0 版本改为 caching_sha2_password
  • 8.4 版本移除了该参数,默认插件固定为 caching_sha2_password

因此,在 MySQL 8.4 中,服务端会调用 caching_sha2_password 插件向客户端发送握手包(handshake packet),包含通信协议版本、服务端版本、随机盐值、能力标志、字符集和密码认证插件名称等信息。

三、客户端收到握手包后,根据包中指定的认证插件(caching_sha2_password)生成并返回握手响应包(handshake response),包括客户端能力标志、用户名、加密密码、默认库名和客户端使用的认证插件。

四、服务端收到响应包后,调用 parse_client_handshake_packet() 函数处理:

  1. 读取客户端能力标志
  2. 如需 SSL,先完成 SSL 握手
  3. 设置客户端字符集
  4. 提取用户名、密码、默认库名和认证插件
  5. 调用 find_mpvio_user 初始化 mpvio(存储认证过程中的用户信息和插件状态)

find_mpvio_user 会根据客户端提供的用户名和 host/ip,从 ACL 用户缓存(mysql.user)中查找对应记录。如果用户不存在,MySQL 不会直接返回"用户名不存在",而是执行 decoy_user() 逻辑,为这类未知用户随机分配一个认证插件,构造一个伪用户记录,以防止外部探测真实用户名。

decoy_user() 函数实现如下:

ACL_USER *decoy_user(const LEX_CSTRING &username, const LEX_CSTRING &hostname,
                     MEM_ROOT *mem, struct rand_struct *rand,
                     bool is_initialized) {
  ...
  if (is_initialized) {
    // 根据用户名和 host/ip 生成 key
    Auth_id key(user);     
    uint value;

    // 如果该 unknown user 已存在,复用之前分配的认证插件
    if (unknown_accounts->find(key, value)) {
      user->plugin = Cached_authentication_plugins::cached_plugins_names[value];
    } else {
      // 首次遇到的 unknown user,从 cached_plugins_names 随机分配认证插件
      const int DECIMAL_SHIFT = 1000;
      const int random_number = static_cast<int>(my_rnd(rand) * DECIMAL_SHIFT);
      uint plugin_num = (uint)(random_number % ((uint)PLUGIN_LAST));
      user->plugin = Cached_authentication_plugins::cached_plugins_names[plugin_num];

      unknown_accounts->clear_if_greater(MAX_UNKNOWN_ACCOUNTS);
      // 记录客户端及分配的插件到 unknown_accounts 缓存
      if (!unknown_accounts->insert(key, plugin_num)) {
        if (!unknown_accounts->find(key, plugin_num))
          user->plugin = default_auth_plugin_name;
        else
          user->plugin = Cached_authentication_plugins::cached_plugins_names[plugin_num];
      }
    }
  }
  ...
  return user;
}

// cached_plugins_names 定义
const LEX_CSTRING Cached_authentication_plugins::cached_plugins_names[(uint)PLUGIN_LAST] = {
  {STRING_WITH_LEN("caching_sha2_password")},
  {STRING_WITH_LEN("mysql_native_password")},
  {STRING_WITH_LEN("sha256_password")}
};

对于 u1 用户,可能随机分配到 caching_sha2_password 或 sha256_password 插件;而 u2 用户恰好分配到了 mysql_native_password。

五、在 parse_client_handshake_packet(),如果客户端使用的密码认证插件与 mysql.user 表记录(或伪用户分配的插件)不一致,MySQL 会调用 do_auth_once() 进行二次认证,使用实际插件进行验证。

如果指定插件在服务端不存在,则触发 Plugin 'xxx' is not loaded 错误。

do_auth_once() 实现如下:

static int do_auth_once(THD *thd, const LEX_CSTRING &auth_plugin_name,
                        MPVIO_EXT *mpvio) {
  DBUG_TRACE;
  int res = CR_OK, old_status = MPVIO_EXT::FAILURE;
  bool unlock_plugin = false;

  // 从缓存获取指定插件
  plugin_ref plugin = g_cached_authentication_plugins->get_cached_plugin_ref(&auth_plugin_name);

  // 缓存不存在则按名称加载
  if (!plugin) {
    if ((plugin = my_plugin_lock_by_name(thd, auth_plugin_name, MYSQL_AUTHENTICATION_PLUGIN)))
      unlock_plugin = true;
  }

  mpvio->plugin = plugin;
  old_status = mpvio->status;

  // 插件存在则调用 authenticate_user() 进行认证交互
  if (plugin) {
    st_mysql_auth *auth = (st_mysql_auth *)plugin_decl(plugin)->info;
    res = auth->authenticate_user(mpvio, &mpvio->auth_info);
    if (unlock_plugin) plugin_unlock(thd, plugin);
  } else {
    // 插件无法加载,触发 Plugin xxx is not loaded 错误
    Host_errors errors;
    errors.m_no_auth_plugin = 1;
    inc_host_errors(mpvio->ip, &errors);
    my_error(ER_PLUGIN_IS_NOT_LOADED, MYF(0), auth_plugin_name.str);
    res = CR_ERROR;
  }
  ...
  return res;
}

具体到 u2 用户,由于分配到的认证插件是 mysql_native_password,在二次认证阶段 MySQL 尝试使用该插件验证。但在 MySQL 8.4 中,mysql_native_password 默认被禁用,因此触发了 ERROR 1524 (HY000): Plugin 'mysql_native_password' is not loaded 错误。

总结

当客户端使用不存在的用户名连接 MySQL 时:

  • MySQL 不会直接提示用户不存在,而是构造伪用户并随机分配认证插件,以防止用户名枚举攻击。
  • 在 MySQL 8.4 中,由于默认禁用 mysql_native_password 插件:
    • 如果随机分配到该插件,会触发 ERROR 1524 (HY000): Plugin 'mysql_native_password' is not loaded 错误。
    • 如果分配到其他插件(如 caching_sha2_password),则走完整认证流程后返回 Access denied for user 'xxx'@'xxx' 错误。

这就是 u1 和 u2 用户报错不同的根本原因。

您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-1 14:12 , Processed in 0.059839 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表