跳转至

当客户端输入 mysql -h x.x.x.x -u root -p 时,服务端在做什么(中)

Conventions

  • 每一个代码块的顶部都有它所属的文件(相对)路径,如果代码块属于某个函数(方法),那么顶部会有函数(方法)的声明。
  • 代码块中的对象,如果很重要,会在行尾补充该对象的声明。
  • 我使用的MySQL 版本是 8.0.41。你看到这篇时,可能有了更新的版本,比如 8.0.42,区别不大的。

身份认证

sql/sql_connect.cc
bool thd_prepare_connection(THD *thd) {
  thd->enable_mem_cnt();

  bool rc;
  lex_start(thd);
  rc = login_connection(thd);

  if (rc) return rc;

  prepare_new_connection_state(thd);
  return false;
}

thd_prepare_connection 方法很简短,一眼就看到第 893 行的 login_connection 是关键。

login_connection 方法也很简短,但如果每个方法都完整列出来,代码块会占很大的篇幅,所以接下来的几个方法,直接展示调用链,因为真的不重要,重要的肯定会贴出来的。

sql/sql_connect.cc
  error = check_connection(thd);
sql/sql_connect.cc
  auth_rc = acl_authenticate(thd, COM_CONNECT);
sql/auth/sql_authentication.cc
    res = do_auth_once(thd, auth_plugin_name, &mpvio);
sql/auth/sql_authentication.cc
    res = auth->authenticate_user(mpvio, &mpvio->auth_info); // (1)!
  1. st_mysql_auth *auth

login_connection -> check_connection -> acl_authenticate -> do_auth_once -> authenticate_user 这么一路调用下来,走到上面这行停住。

auth 是指向结构类型的指针,表示身份认证插件,authenticate_user 是该结构的成员,对应类型 int (*)(MYSQL_PLUGIN_VIO *vio,MYSQL_SERVER_AUTH_INFO *info),这是指向身份认证函数的指针类型。

不同的身份认证插件会有不同的实现,我们只看默认插件 caching_sha2_password 的实现。caching_sha2_password 在文件 sql/auth/sha2_password.cc 中。caching_sha2_password_authenticate 函数是身份认证函数在该插件中的具体实现,也就是说 authenticate_user 指向了这个函数,所以接下来我们会看这个函数。

Tip

MySQL 有 3 种身份认证插件,分别是 mysql_native_passwordsha256_passwordcaching_sha2_password

握手

TCP 的握手大家肯定都听过,这儿讲的是 TCP 握手之后,MySQL 实现的服务端与客户端的握手。下面我们通过 Wireshark 查看这个过程。

握手

1、2、3 号包是 TCP 的握手,4 号和 6 号是 MySQL 的握手。

4 号包是 MySQL 服务端发出的,说明握手是服务端发起。上图中左下方有包内容,可以看到服务端通过握手传递了版本号、协议版本、盐、身份认证的插件等信息,对应下面函数:

sql/auth/sha2_password.cc
  if (vio->write_packet(vio, (unsigned char *)scramble, SCRAMBLE_LENGTH + 1))

write_packet 是指向函数的指针,指向下面的函数 server_mpvio_write_packet

sql/auth/sql_authentication.cc
/**
  vio->write_packet() callback method for server authentication plugins

  This function is called by a server authentication plugin, when it wants
  to send data to the client.

  It transparently wraps the data into a handshake packet,
  and handles plugin negotiation with the client. If necessary,
  it escapes the plugin data, if it starts with a mysql protocol packet byte.
*/
static int server_mpvio_write_packet(MYSQL_PLUGIN_VIO *param,
                                     const uchar *packet, int packet_len) {
  MPVIO_EXT *mpvio = (MPVIO_EXT *)param;
  int res;
  Protocol_classic *protocol = mpvio->protocol;

  DBUG_TRACE;
  /*
    Reset cached_client_reply if not an old client doing mysql_change_user,
    as this is where the password from COM_CHANGE_USER is stored.
  */
  if (!((!(protocol->has_client_capability(CLIENT_PLUGIN_AUTH))) &&
        mpvio->status == MPVIO_EXT::RESTART &&
        mpvio->cached_client_reply.plugin ==
            ((st_mysql_auth *)(plugin_decl(mpvio->plugin)->info))
                ->client_auth_plugin))
    mpvio->cached_client_reply.pkt = nullptr;
  /* for the 1st packet we wrap plugin data into the handshake packet */
  if (mpvio->packets_written == 0)
    res = send_server_handshake_packet(
        mpvio, pointer_cast<const char *>(packet), packet_len);
  else if (mpvio->status == MPVIO_EXT::RESTART) {
    /*
      Inject error here for testing purpose.
      See auth_sec.server_send_client_plugin
    */
    DBUG_EXECUTE_IF("assert_authentication_roundtrips", {
      return -1;  // Crash here.
    });
    res = send_plugin_request_packet(mpvio, packet, packet_len);
  } else if (mpvio->status == MPVIO_EXT::START_MFA) {
    res = send_auth_next_factor_packet(mpvio, packet, packet_len);
    /*
      reset the status to avoid sending AuthNextFactor again for the
      same factor authentication.
    */
    mpvio->status = MPVIO_EXT::FAILURE;
  } else
    res = wrap_plguin_data_into_proper_command(protocol->get_net(), packet,
                                               packet_len);
  mpvio->packets_written++;
  return res;
}

关键在第 3241 行 send_server_handshake_packet 函数,函数名翻译成中文就是发送服务端握手包,对应 Wireshark 截图中的 4 号包。

sql/auth/sha2_password.cc
  if ((pkt_len = vio->read_packet(vio, &pkt)) == -1) return CR_AUTH_HANDSHAKE;
---
title: "MySQL Server Greeting Packet"
---
packet-beta
0-23: "Packet Length: 80"
24-31: "Packet Number: 0"
32-39: "Protocol: 10"
40-143: "Version: 8.0.41-debug"
144-175: "Thread ID: 8"
176-247: "Salt: C@o\\x13\\x14IeR"
248-263: "Server Capabilities: ffff"
264-271: "Server Language: utf8mb4"
272-287: "Server Status: 0002"
288-303: "Extended Server Capabilities: dfff"
304-311: "Authentication Plugin Length: 21"
312-391: "Unused: 00 00 00 00 00 00 00 00 00 00"
392-495: "Salt: tB0.9HRb,Zj/"
496-671: "Authentication Plugin: caching_sha2_password"

完成发送服务端握手包后,函数返回到 caching_sha2_password_authenticate。接着,往下走到第 962 行的 read_packet 函数,这也是指向函数的指针,指向 sql/auth/sql_authentication.ccserver_mpvio_read_packet 函数,这个函数稍稍有点长,不贴出来了。我们只需要关心,在该函数中调用了下面的 parse_client_handshake_packet 函数,即解析客户端握手包,这个包对应 Wireshark 截图中的 6 号包。

sql/auth/sql_authentication.cc
static size_t parse_client_handshake_packet(THD *thd, MPVIO_EXT *mpvio,
                                            uchar **buff, size_t pkt_len) {
// ignore
  /*
    If client requested SSL then we must stop parsing, try to switch to SSL,
    and wait for the client to send a new handshake packet.
    The client isn't expected to send any more bytes until SSL is initialized.
  */
  if (protocol->has_client_capability(CLIENT_SSL)) { // line 2882
// ignore
    if (sslaccept(*(context.get()), protocol->get_vio(), // line 2900
                  protocol->get_net()->read_timeout, &errptr)) {
      DBUG_PRINT("error", ("Failed to accept new SSL connection"));
      return packet_error;
    }

    DBUG_PRINT("info", ("Reading user information over SSL layer"));
    int rc = protocol->read_packet(); // line 2907
    pkt_len = protocol->get_packet_length();
  }
// ignore
  char *user = get_string(&end, &bytes_remaining_in_packet, &user_len); // line 2999
// ignore
  passwd =
      get_length_encoded_string(&end, &bytes_remaining_in_packet, &passwd_len); // line 3011

在解析客户端端握手包过程中,如果发现客户端支持 TLS(第 2882 行),那么会进行 TLS 握手(第 2900 行),TLS 握手完成后客户端会发送用户名和密码,服务端阻塞读取(第 2907 行),然后解析(第 2999 和 3011 行)得到用户名和密码。

小结

MySQL 服务端和客户端在 TCP 握手完成后,并没有立刻进行 TLS 握手,而是先做了基础信息的交换,比如版本号、是否支持 TLS 等信息,这是 MySQL 自身实现的握手,如果支持 TLS,那么才进行 TLS 握手,全部完成后,客户端发送用户名和密码,服务端接收到后解析。

下一篇,讲讲服务端如何验证用户名和密码。