共计 5338 个字符,预计需要花费 14 分钟才能阅读完成。
欢送来到 GreatSQL 社区分享的 MySQL 技术文章,如有疑难或想学习的内容,能够在下方评论区留言,看到后会进行解答
0. 导读
雷同的账号、明码,手动客户端连贯能够胜利,通过 MySQL Connectors 却失败了,为什么?
1. 景象形容
通过 MySQL C API 编写的一个程序,在进行用户登录操作的时候,程序报错,登录失败。
然而如果通过 mysql 客户端,手动登录胜利后,再启动客户端程序,不再报错,程序运行胜利。
2. 抓包剖析问题
学会抓包,就超过了 90% 的程序员。sudo tcpdump -i any tcp and port xxx -s 1500 -w filename -v
C 程序登录失败时的包
前两个包很失常,第三个包第一次见,wireshark 解析的叫做 AuthSwitchRequest.
看下这个包的内容。
mysql 客户端登录胜利后,再执行 C 程序登录胜利时的包
看下这个 AuthSwithRequest 包的内容。
认证胜利和失败的包数据是不同的,别离是 03 和 04。
3. AuthSwitchRequest 包
首先看下 AuthSwitchRequest 包的官网解释。
Protocol::AuthSwitchRequest:
Authentication Method Switch Request Packet. If both server and client support CLIENT_PLUGIN_AUTH capability, server can send this packet to ask client to use another authentication method.
Payload
1 [fe]
string[NUL] plugin name
string[EOF] auth plugin data
能够看出,这个包 payload 的第一个字节是 0xfe,与抓包中的 01 是不同的,而且 AuthSwitchRequest 前面跟的应该是 plugin name,是个字符串,而抓包中,内容是 04。
所以 wireshard 解包谬误了。
在协定外面咱们找下 payload 第一个字节是 01 的包,找到了 AuthMoreData 的包。
Protocol::AuthMoreData:
Payload
1 [01]
string[EOF] plugin data
这个包的阐明 payload 前面是 string 类型,和咱们的抓包也是有些出入,先不论这个了。
跟到这里只能去代码外面找下这个包是什么。
4. 服务器认证
目前用户认证默认都走 caching_sha2_password 的 plugin,之前版本都是 mysql_native_password。
mysql > select user,host,plugin from mysql.user;
+------------------+-----------+-----------------------+
| user | host | plugin |
+------------------+-----------+-----------------------+
| mysql.infoschema | localhost | caching_sha2_password |
| mysql.session | localhost | caching_sha2_password |
| mysql.sys | localhost | caching_sha2_password |
| root | localhost | caching_sha2_password |
+------------------+-----------+-----------------------+
mysql > show variables like '%auth%';
+-------------------------------+-----------------------+
| Variable_name | Value |
+-------------------------------+-----------------------+
| default_authentication_plugin | caching_sha2_password |
+-------------------------------+-----------------------+
1 row in set (0.01 sec)
服务器端认证代码在 sql/auth/sha2password.cc 文件中的 caching_sha2_password_authenticate 函数。
在此函数中,找到了发包的代码,正好能够对应到咱们抓包的 AuthMoreData。
996 if (pkt_len != sha2_password::CACHING_SHA2_DIGEST_LENGTH) return CR_ERROR;
997
998 std::pair<bool, bool> fast_auth_result =
999 g_caching_sha2_password->fast_authenticate(1000 authorization_id, reinterpret_cast<unsigned char *>(scramble),
1001 SCRAMBLE_LENGTH, pkt,
1002 info->additional_auth_string_length ? true : false);
1003
1004 if (fast_auth_result.first) {
1005 /*
1006 We either failed to authenticate or did not find entry in the cache.
1007 In either case, move to full authentication and ask the password
1008 */
1009 if (vio->write_packet(vio, (uchar *)&perform_full_authentication, 1))
1010 return CR_AUTH_HANDSHAKE;
1011 } else {
1012 /* Send fast_auth_success packet followed by CR_OK */
1013 if (vio->write_packet(vio, (uchar *)&fast_auth_success, 1))
1014 return CR_AUTH_HANDSHAKE;
1015 if (fast_auth_result.second) {
1016 const char *username =
1017 *info->authenticated_as ? info->authenticated_as : "";
1018 LogPluginErr(INFORMATION_LEVEL,
1019 ER_CACHING_SHA2_PASSWORD_SECOND_PASSWORD_USED_INFORMATION,
1020 username, hostname ? hostname : "");
1021 }
1022
1023 return CR_OK;
1024 }
1025
首先进行了 fast_authenticate,依据这个后果 fast_auth_result.first,别离发送了不同的包 perform_full_authentication 和 fast_auth_success。
791 static char request_public_key = '\2';
792 static char fast_auth_success = '\3';
793 static char perform_full_authentication = '\4';
能够看到咱们 C 程序登录失败时,给咱们发送的是 perform_full_authentication,而认证胜利发送的是 fast_auth_success 包。
什么状况下会呈现 perform_full_authentication 包呢?代码中给出了阐明,咱们就不去看 fast_authenticate 的代码逻辑了,从阐明就能理解到大略状况。
1006 We either failed to authenticate or did not find entry in the cache. 1007 In either case, move to full authentication and ask the password
也就是说,如果 cache 中没有记录,或者认证失败,会进入 perform_full_authentication 流程。咱们从这个认证插件的名字 caching_sha2_password 就能够晓得,这是个带 cache 的认证插件。
而这正好解释了为什么用 mysql 客户端手动登陆后,咱们 C 程序就登录胜利了,因为 cache 中曾经有了记录。
那么:为什么手动客户端认证就能胜利呢?而咱们本人写的 C 程序就会失败呢?
且看客户端认证剖析。
5. 客户端认证
客户端认证的代码逻辑在 sql-common/client_authentication.cc 中的 caching_sha2_password_auth_client 函数中。
514 if (pkt_len != 1 || *pkt != perform_full_authentication) {515 DBUG_PRINT("info", ("Unexpected reply from server."));
516 return CR_ERROR;
517 }
518
519 /* If connection isn't secure attempt to get the RSA public key file */
520 if (!connection_is_secure) {521 public_key = rsa_init(mysql);
......
523 if (public_key == NULL && mysql->options.extension &&
524 mysql->options.extension->get_server_public_key) {
525 // If no public key; request one from the server.
......
540 }
541
542 if (public_key) {
543 /*
......
584 } else {
585 set_mysql_extended_error(mysql, CR_AUTH_PLUGIN_ERR, unknown_sqlstate,
586 ER_CLIENT(CR_AUTH_PLUGIN_ERR),
587 "caching_sha2_password",
588 "Authentication requires secure connection.");
589 return CR_ERROR;
590 }
591 } else {
592 /* The vio is encrypted already; just send the plain text passwd */
593 if (vio->write_packet(vio, (uchar *)mysql->passwd, passwd_len))
594 return CR_ERROR;
595 }
能够看到客户端收到 perform_full_authentication 包后,依据 connection_is_secure 进行了分支。
咱们的客户端中的连贯必定是没有开启 SSL 的,所以会走进 if (!connection_is_secure) 流程。
mysql 客户端登录默认是开启 SSL 认证的,故走了 else 流程。
咱们应用 mysql 客户端登录时的命令如下,是开启了 ssl 的:
shell> bin/mysql -h127.0.0.1 -uroot -P3301 -ppassword
如果客户端想禁用 ssl,须要加上–ssl-mode=disable 选项。
所以,这就解释了为什么客户端能登录胜利了。
当初咱们看下 if (!connection_is_secure) 流程,发现没有进入上面的 if 流程,这样就不会生成 public_key,导致 public_key 为空。
if (public_key == NULL && mysql->options.extension &&
524 mysql->options.extension->get_server_public_key)
具体来说,是因为咱们的连贯选项没有设置 mysql->options.extension->get_server_public_key。
6. 解决办法
在不开启 ssl 选项的时候,咱们须要设置 get_server_public_key 选项。
+ bool get_server_public_key = true;
+ mysql_options(m_client, MYSQL_OPT_GET_SERVER_PUBLIC_KEY,
+ &get_server_public_key);
参考文档
connection-phase-packets, https://dev.mysql.com/doc/int…
authentication method mismatch, https://dev.mysql.com/doc/int…
Enjoy GreatSQL :)
本文由博客一文多发平台 OpenWrite 公布!