在MySQL 8.4中,如果启用了`mysql_native_password`插件但未正确加载,可能会遇到错误信息Plugin 'mysql_native_password' is not loaded。以下是一些可能的解决步骤:1.

摘要:现象 最近遇到一个有趣的案例:在一个新创建的 MySQL 8.4 实例中,使用用户 u2 登录时,返回了Plugin 'mysql_native_password' is not loaded
现象 最近遇到一个有趣的案例:在一个新创建的 MySQL 8.4 实例中,使用用户 u2 登录时,返回了Plugin 'mysql_native_password' is not loaded错误。 $ mysql -h127.0.0.1 -P3316 -uu2 -p123 mysql: [Warning] Using a password on thecommandline interface can be insecure. ERROR 1524 (HY000): Plugin'mysql_native_password'is not loaded 奇怪的是,检查mysql.user表后却发现: 实例里并没有 u2 这个用户; 现有用户中并没有用户在使用 mysql_native_password。 mysql>selecthost,user,pluginfrommysql.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 inset(0.05sec) 有意思的是,同样是不存在,如果使用 u1 登录,返回的却是Access denied for user 'xxx'@'xxx'错误: $ mysql -h127.0.0.1 -P3316 -uu1 -p123 mysql: [Warning] Using a password on thecommandline interface can be insecure. ERROR 1045 (28000): Access deniedforuser'u1'@'127.0.0.1'(using password: YES) 问题来了: 同样不存在,为什么 u1 和 u2 会返回不同的错误? 明明没有用户在使用 mysql_native_password,为什么 MySQL 会提示Plugin 'mysql_native_password' is not loaded? 根因分析 下面结合 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 中,MySQL 会调用 caching_sha2_password 插件向客户端发送一个握手包(handshake packet)。 握手包的内容包括通信协议版本、服务端版本、随机数(盐值)、服务端能力标志、默认字符集编号、密码认证插件名称等。 三、 客户端收到握手包后,默认会根据包中指定的认证插件(在 MySQL 8.4 中是 caching_sha2_password)生成并返回一个握手响应包(handshake response)。 响应包的内容包括客户端能力标志、用户名、加密后的密码、要连接的库名、客户端使用的认证插件等。 四、 服务端收到响应包后,会调用parse_client_handshake_packet()函数进行处理。该函数主要做: 读取客户端能力标志。 如果客户端要求 SSL,则先完成 SSL 握手并重新读取一个包。 设置客户端使用的字符集。 提取用户名、密码、默认库名和认证插件。 调用find_mpvio_user初始化 mpvio( mpvio 用于存储认证过程中的用户信息、连接信息及插件交互状态)。 find_mpvio_user会根据客户端发来的用户名与 host/ip,从 ACL 用户缓存(mysql.user)中找到对应的用户记录。如果用户不存在,MySQL 不会直接暴露“用户名不存在”,而是走decoy_user()逻辑,为这类“未知用户”随机分配一个认证插件,构造一个看起来正常的用户记录。这样可以避免外部探测哪些用户名真实存在。 以下是decoy_user()函数的具体实现。 ACL_USER *decoy_user(constLEX_CSTRING &username,constLEX_CSTRING &hostname, MEM_ROOT *mem, struct rand_struct *rand, boolis_initialized){ ... if(is_initialized) { // 根据用户名和 host/ip 生成一个 key Auth_idkey(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 中随机分配一个认证插件。 constintDECIMAL_SHIFT =1000; constintrandom_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]; } } } ... returnuser; } // cached_plugins_names 的定义 constLEX_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()进行二次认证。 此时,认证会使用mysql.user表中的插件,或者伪用户被随机分配的插件。 如果指定的认证插件在服务端不存在,则会触发Plugin 'xxx' is not loaded错误。 do_auth_once()的具体实现如下: staticintdo_auth_once(THD *thd,constLEX_CSTRING &auth_plugin_name, MPVIO_EXT *mpvio){ DBUG_TRACE; intres = CR_OK, old_status = MPVIO_EXT::FAILURE; boolunlock_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; } ... returnres; } 具体到 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错误。 若分配到其他插件,则会走完整认证流程并返回Access denied for user 'xxx'@'xxx'错误。 以上就是 u1 和 u2 报错不同的根本原因。