Analyse des pannes | D'un journal d'erreurs à une analyse approfondie du mécanisme d'authentification MySQL et des bogues

Auteur : Li Xichao

Un ingénieur de base de données de Jiangsu Suning Bank qui aime rire, principalement responsable de l'exploitation et de la maintenance quotidiennes de la base de données, de la construction de l'automatisation et de l'exploitation et de la maintenance de la plate-forme DMP. Bon à MySQL, Python, Oracle, comme le cyclisme et la technologie de recherche.
Source de cet article : contribution originale

*Produit par la communauté open source Aikesheng, le contenu original ne peut être utilisé sans autorisation, veuillez contacter l'éditeur et indiquer la source pour la réimpression.


Les étudiants en R&D ont signalé que le système d'entreprise lié à la base de données MySQL dans un environnement de test de performances du système fonctionne normalement, mais qu'il existe un grand nombre de journaux d'avertissement et qu'il est nécessaire de coopérer à l'analyse des raisons.

1. Phénomènes anormaux

Il existe un grand nombre des informations suivantes dans le fichier journal des erreurs mysql :

2023-01-10T01:07:23.035479Z 13 [Warning] [MY-013360] [Server] Plugin sha256_password reported: ''sha256_password' is deprecated and will be removed in a future release. Please use caching_sha2_password instead'

informations clés sur l'environnement

2. Analyse préliminaire

Lorsque vous voyez le journal d'avertissement ci-dessus, selon la théorie empirique, la première réaction devrait être que la version du client est trop basse et que son plug-in d'authentification est une version que le serveur supprimera, de sorte que le message d'avertissement ci-dessus est généré. En particulier, certains outils clients courants peuvent facilement déclencher ce problème en raison de la fréquence de mise à jour.

essayer de reproduire

Selon les suggestions d'analyse préliminaire, après avoir communiqué les suggestions d'analyse préliminaire aux étudiants en R&D, accédez à la base de données via des outils de base de données communs pour voir si l'erreur peut être reproduite. Cependant, grâce aux utilisateurs communs de la base de données et à l'accès à la base de données via différents outils, l'exception n'a pas été déclenchée au moment de l'accès.
Par conséquent, la première tentative de reproduction a échoué. Est-ce pour d'autres raisons ?

Lors du processus de tentative d'accès pour la première fois, observez le journal des erreurs de la base de données en temps réel. Lors de la tentative d'accès avec le client, l'erreur ne s'est pas reproduite. Mais voyez toujours que le journal d'avertissement correspondant est généré en continu dans le fichier journal des erreurs. De plus, la fréquence est élevée et le temps d'intervalle est fixe, ce qui prouve également que l'erreur n'est pas accessible manuellement par les outils de base de données.

Le système d'application fonctionne normalement et cela n'est pas causé par le client ! En tant que DBA, comment devriez-vous l'analyser plus en détail ?

Premières astuces

En raison de l'environnement de test, pour cette erreur, vous pouvez effectuer les opérations suivantes pour activer les journaux généraux de MySQL :

-- 开通一般日志:
show variables like 'general_log';
set global general_log=on;
show variables like 'general_log';
-- 查看一般日志路径:
show variables like 'general_log_file';

Après avoir activé le journal, observez le journal des erreurs et recherchez les enregistrements suivants dans le journal général :

Conseil : Après avoir trouvé une exception, fermez immédiatement le journal général pour éviter de générer trop de journaux et d'épuiser l'espace disque :

-- 开通一般日志:
show variables like 'general_log';
set global general_log=off;
show variables like 'general_log';

Autrement dit, l'utilisateur dbuser2 lance une demande d'accès à la base de données à partir du serveur 10.xy43 au moment du problème. Après avoir confirmé l'utilisateur et le serveur d'accès anormal, vérifiez la table mysql.user de la base de données, les tables skip-grant-tables et les autres configurations, et constatez que l'utilisateur n'existe pas dans la base de données et que la table d'autorisation et les autres configurations ne sont pas ignorées. L'utilisation de cet utilisateur ne pourra pas se connecter à la base de données.

Faites remonter les informations aux étudiants en recherche et développement et confirmez rapidement que certaines applications ont été configurées de manière déraisonnable, en utilisant des utilisateurs de base de données inexistants et en se connectant régulièrement à la base de données pour effectuer des tâches. Ainsi, après que les étudiants en recherche et développement ont modifié la configuration, le journal d'avertissement n'a plus été généré.

Donc l'analyse de ce problème est là, peut-elle être terminée ?

Après modification de la configuration, le journal d'avertissement ne se produit plus ! Mais puisqu'il s'agit d'un utilisateur inexistant, pourquoi est-il invité à supprimer le plug-in d'authentification lors de l'accès ?

3. Analyse du code source

En cas de questions, la première chose qui vient à l'esprit est la suivante : puisque l'utilisateur de la base de données n'existe pas dans la table mysql.user, la connexion générera également un avertissement. Cet utilisateur est-il un utilisateur interne de mysql et est-il codé en dur ? Récupérez donc le code source de la version correspondante et confirmez-le avec la commande suivante :

cd mysql-8.0.27/
grep -rwi "dbuser2" *

Le résultat d'accès est vide, c'est-à-dire qu'il n'y a pas d'"utilisateur interne" deviné.

Logique d'authentification de connexion normale

Puisqu'il n'y a pas de codage en dur, cela ne peut être causé que par une logique interne. Donc, tout d'abord, pour le processus de connexion de l'utilisateur mysql dans des circonstances normales, les résultats de l'analyse du code source sont les suivants :

|—> handle_connection
  |—> thd_prepare_connection
    |—> login_connection
      |—> check_connection
        // 判断客户端的主机名是否可以登录(mysql.user.host),如果 mysql.user.host 有 '%' 那么将 allow_all_hosts,允许所有主机。
        |—> acl_check_host   
        |—> acl_authenticate
          |—> server_mpvio_initialize // 初始化mpvio对象,包括赋值 mpvio->ip / mpvio->host
          |—> auth_plugin_name="caching_sha2_password"
          |—> do_auth_once
            |—> caching_sha2_password_authenticate // auth->authenticate_user
              |—> server_mpvio_read_packet // vio->read_packet(vio, &pkt) // pkt=passwd
                |—> parse_client_handshake_packet
                  |—> char *user = get_string(&end, &bytes_remaining_in_packet, &user_len);
                  |—> passwd = get_length_encoded_string(&end, &bytes_remaining_in_packet, &passwd_len);
                  |—> mpvio->auth_info.user_name = my_strndup(key_memory_MPVIO_EXT_auth_info, user, user_len, MYF(MY_WME))
                  // 根据 user 搜索 mysql.user.host ,并与客户端的 hostname/ip 进行比较:匹配记录后,赋值 mpvio->acl_user
                  |—> find_mpvio_user(thd, mpvio) 
                    |—> list = cached_acl_users_for_name(mpvio->auth_info.user_name); // 根据 user 搜索 mysql.user.host 
                    |—> acl_user_tmp->host.compare_hostname(mpvio->host, mpvio->ip) //  与客户端的 hostname/ip 进行比较
                    |—> mpvio->acl_user_plugin = mpvio->acl_user->plugin; // 赋值 acl_user_plugin 属性为用户的plugin名
                    |—> mpvio->auth_info.multi_factor_auth_info[0].auth_string = mpvio->acl_user->credentials[PRIMARY_CRED].m_auth_string.str; 
                    |—> mpvio->auth_info.auth_string = mpvio->auth_info.multi_factor_auth_info[0].auth_string; 
                  |—> if (my_strcasecmp(system_charset_info, mpvio->acl_user_plugin.str,plugin_name(mpvio->plugin)->str) != 0)
                  |—> my_strcasecmp(system_charset_info, client_plugin,user_client_plugin_name) //检查客户端的认证插件与用户插件是否相同
              |—> make_hash_key(info->authenticated_as, hostname ? hostname : nullptr, authorization_id);  // 生成 authorization_id = user1\000% 
              |—> g_caching_sha2_password->fast_authenticate(authorization_id,*scramble,20,pkt,false) // 进行快速授权操作
                |—> m_cache.search(authorization_id, digest) // 根据 user、host 搜索密码,赋值到digest
                |—> Validate_scramble validate_scramble_first(scramble, digest.digest_buffer[0], random, random_length);
                |—> validate_scramble_first.validate(); // 校验 scramble
              // 如验证成功
              |—> vio->write_packet(vio, (uchar *)&fast_auth_success, 1)
              |—> return CR_OK;
              // 否则进行进行慢授权操作
              |—> g_caching_sha2_password->authenticate( authorization_id, serialized_string, plaintext_password);
          |—> server_mpvio_update_thd(thd, &mpvio);
          |—> check_and_update_password_lock_state(mpvio, thd, res);
          // 继续其它授权操作

C'est-à-dire que l'opération d'authentification principale est effectuée dans la fonction caching_sha2_password_authenticate(), appelez d'abord la fonction find_mpvio_user(), recherchez l'utilisateur configuré via l'utilisateur et le nom d'hôte, puis appelez la fonction fast_authenticate() pour vérifier rapidement le mot de passe.

Utiliser une logique d'authentification utilisateur inexistante

Lorsque l'utilisateur n'existe pas, le processus de connexion de l'utilisateur mysql, les résultats de l'analyse du code source sont les suivants :

|—> handle_connection
  |—> thd_prepare_connection
    |—> login_connection
      |—> check_connection
        // 判断客户端的主机名是否可以登录(mysql.user.host),如果 mysql.user.host 有 '%' 那么将 allow_all_hosts,允许所有主机。
        |—> acl_check_host   
        |—> acl_authenticate
          |—> server_mpvio_initialize // 初始化mpvio对象,包括赋值 mpvio->ip / mpvio->host
          |—> auth_plugin_name="caching_sha2_password"
          |—> do_auth_once
            |—> caching_sha2_password_authenticate // auth->authenticate_user
              |—> server_mpvio_read_packet // vio->read_packet(vio, &pkt) // pkt=passwd
                |—> parse_client_handshake_packet
                  |—> char *user = get_string(&end, &bytes_remaining_in_packet, &user_len);
                  |—> passwd = get_length_encoded_string(&end, &bytes_remaining_in_packet, &passwd_len);
                  |—> mpvio->auth_info.user_name = my_strndup(key_memory_MPVIO_EXT_auth_info, user, user_len, MYF(MY_WME))
                  |—> find_mpvio_user(thd, mpvio) 
                    |—> list = cached_acl_users_for_name(mpvio->auth_info.user_name); // 根据 user 搜索 mysql.user.host, 由于用户不存在,搜索不到记录
                    |—> mpvio->acl_user = decoy_user(usr, hst, mpvio->mem_root, mpvio->rand, initialized); // 
                      |—> Auth_id key(user);
                      // 判断是否用户存在于 unknown_accounts
                      |—> unknown_accounts->find(key, value)
                      // 如存在:
                      |—> user->plugin = Cached_authentication_plugins::cached_plugins_names[value];
                      // 如不存在:
                      |—> 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->insert(key, plugin_num)
                    |—> mpvio->acl_user_plugin = mpvio->acl_user->plugin; // 赋值 acl_user_plugin 属性为用户的plugin名
                    |—> mpvio->auth_info.multi_factor_auth_info[0].auth_string = mpvio->acl_user->credentials[PRIMARY_CRED].m_auth_string.str; // ""
                    |—> mpvio->auth_info.auth_string = mpvio->auth_info.multi_factor_auth_info[0].auth_string; // ""
                    |—> mpvio->auth_info.additional_auth_string_length = 0; // 0
                    |—> mpvio->auth_info.auth_string_length = mpvio->auth_info.multi_factor_auth_info[0].auth_string_length; // 0
                  |—> if (my_strcasecmp(system_charset_info, mpvio->acl_user_plugin.str,plugin_name(mpvio->plugin)->str) != 0)
                  |—> return packet_error;
                |—> if (pkt_len == packet_error) goto err;
                |—> return -1;
          |—> auth_plugin_name = mpvio.acl_user->plugin;
          |—> res = do_auth_once(thd, auth_plugin_name, &mpvio);
            |—> sha256_password_authenticate() //auth->authenticate_user(mpvio, &mpvio->auth_info);
              |—> LogPluginErr // Deprecate message for SHA-256 authentication plugin.
              // 打印: 2023-01-10T01:07:23.035479Z 13 [Warning] [MY-013360] [Server] Plugin sha256_password reported: ''sha256_password' is deprecated and will be removed in a future release. Please use caching_sha2_password instead'
              |—> server_mpvio_read_packet() // vio->read_packet(vio, &pkt)
              |—> if (info->auth_string_length == 0 && info->additional_auth_string_length == 0) // info -> auth_info
              |—>   return CR_ERROR;
            |—> return res; // 0
          |—> server_mpvio_update_thd(thd, &mpvio);
          |—> check_and_update_password_lock_state(mpvio, thd, res); // 直接返回
          ...
          |—> login_failed_error // 打印登录报错信息
          // 2023-01-10T02:02:44.659796Z 19 [Note] [MY-010926] [Server] Access denied for user 'user2'@'localhost' (using password: YES)
      |—> thd->send_statement_status();  // 客户端终止

C'est-à-dire l'objet acl_user créé par la fonction decoy_user() lorsqu'un utilisateur inexistant est utilisé pour se connecter à la base de données. Lorsque cet objet est créé, son attribut de plugin est sélectionné au hasard dans cached_plugins_enum. Ainsi il est possible de sélectionner le plugin PLUGIN_SHA256_PASSWORD. Cependant, à l'entrée de la fonction sha256_password_authenticate(), une invite de niveau Avertissement sera générée pour indiquer que l'authentification PLUGIN_SHA256_PASSWORD sera rejetée. Par la suite, puisque la longueur de l'objet acl_user auth_string_length créé dans decoy_user() n'est pas 0, CR_ERROR sera renvoyé directement dans la logique d'authentification suivante, c'est-à-dire que l'authentification échoue.

résumé des causes profondes

Selon l'analyse certifiée ci-dessus, la cause principale de la suppression de PLUGIN_SHA256_PASSWORD dans le journal des erreurs est : dans la version actuelle, lors de l'utilisation d'un utilisateur inexistant pour se connecter à la base de données, mysql sélectionnera au hasard le plug-in d'authentification par mot de passe de l'utilisateur. , dans la version actuelle , il y a 1/3 de probabilité que le plug-in PLUGIN_SHA256_PASSWORD soit sélectionné. Après avoir sélectionné ce plugin, la logique d'authentification suivante déclenchera la génération de journaux d'avertissement.

4. Résolution de problèmes

Sur la base du processus d'analyse ci-dessus, la cause directe de ce problème est que l'application configure un utilisateur de base de données inexistant, et la cause principale est qu'il existe certains défauts dans la logique d'authentification de connexion à la base de données. Alors pour résoudre ce problème, vous pouvez vous référer aux solutions suivantes :

1. En vous référant à la solution dans l'analyse préliminaire, modifiez la configuration de connexion de l'application aux informations utilisateur correctes ;

2. Vous pouvez filtrer l'alarme à travers les paramètres de la base de données mysql pour éviter de saisir les informations d'alarme dans le fichier journal des erreurs. La configuration pertinente est la suivante :

show variables like 'log_error_suppression_list';
set global log_error_suppression_list='MY-013360;
show variables like 'log_error_suppression_list';

Notez que l'utilisation de ce schéma entraînera également une alerte qui existe et utilise le plug-in d'authentification SHA256_PASSWORD. Peut être utilisé comme solution temporaire ;

3. Modifiez le code mysql pour éviter de sélectionner le plug-in d'authentification SHA256_PASSWORD lors de la connexion à la base de données avec un utilisateur inexistant. Le bogue #109635 est actuellement enregistré pour cette solution.

Pièce jointe : emplacement de la fonction clé

find_mpvio_user() (./sql/auth/sql_authentication.cc:2084)
parse_client_handshake_packet() (./sql/auth/sql_authentication.cc:2990)
server_mpvio_read_packet() (./sql/auth/sql_authentication.cc:3282)
caching_sha2_password_authenticate() (./sql/auth/sha2_password.cc:955)
do_auth_once() (./sql/auth/sql_authentication.cc:3327)
acl_authenticate() (./sql/auth/sql_authentication.cc:3799)
check_connection() (./sql/sql_connect.cc:651)
login_connection() (./sql/sql_connect.cc:716)
thd_prepare_connection() (./sql/sql_connect.cc:889)
handle_connection() (./sql/conn_handler/connection_handler_per_thread.cc:298)

Je suppose que tu aimes

Origine blog.csdn.net/ActionTech/article/details/130088608
conseillé
Classement