数据库内核月报

数据库内核月报 - 2018 / 08

MySQL · 源码分析 · 连接与认证过程

Author: 西加

前言

本文主要介绍 client 与 server 连接与认证过程,包括client和server之间的网络交互情况

使用 mysql 5.6.16官方版本分析

认证流程分析

client 建立连接的认证过程如下图

  1. server 监听端口
  2. client 向server建立TCP连接
  3. server 向client发送挑战码报文(报文详细内容在下文中有分析)
  4. client 使用挑战码加密密码,将加密后的密码包含在回包中,发送给server
  5. server 根据client的回包,验证密码的有效性,给client发送ok包或error包

client 给 server 的回包,和 server 的ok包内容在下面的代码栈里有体现

server 给 client 的挑战码报文下下文中有详细分析

image.png

和其他c语言调用方式一样,mysql client 也是调用 mysql_read_connect 建立连接

mysql_real_connect 实际是 client.c 里面的 CLI_MYSQL_REAL_CONNECT 函数

server监听请求

main

  mysqld_main
    
    handle_connections_sockets
    
      /* 监听socket请求 */
      poll
      
      mysql_socket_accept
      
      create_new_thread

client 向 server 建立连接请求

main
  
  sql_connect
  
    sql_read_connect
    
      mysql_read_connect
       
        vio_socket_connect
        
          inline_mysql_socket_connect
          
            /*向server发送连接请求*/
            connect
        
        /* 等待server回包 */    
        cli_safe_read
          
          my_net_read
        
        /* client 主要认证过程 */
        run_plugin_auth
          
          native_password_auth_client
          
            /* 使用挑战码加密密码 */
            scramble
          
            client_mpvio_write_packet
            
              /* 
                拼接client的返回包,包含:
                  1. client 能力位 
                  2. 最大报文长度
                  3. 字符集
                  4. 用户名
                  5. 挑战码加密后的密码
                  6. database name (如果能力位包含 CLIENT_CONNECT_WITH_DB
                  7. client auth plugin name (如果能力位包含 CLIENT_PLUGIN_AUTH
              
              */
              send_client_reply_packet
              
                /* 发送认证包 */
                my_net_write


handle_one_connection

  do_handle_one_connection
    
    thd_prepare_connection
    
      login_connection
      
        check_connection
          
          /* 检查host或IP是否能匹配某个用户 */
          acl_check_host
        
          acl_authenticate
          
            do_auth_once
            
              native_password_authenticate
                
                /*给 client 发送挑战码*/
                server_mpvio_write_packet
                  
                  /* 发送 handshake 包 */
                  send_server_handshake_packet
              
                /*  */
                server_mpvio_read_packet
                  
                  /* 接收 client 报文 */
                  my_net_read
                  
                  /* 解析client报文 */
                  parse_client_handshake_packet
              
              /* 检查密码有效性 */
              check_scramble
              
        Protocol::end_statement
        
          Protocol::send_ok
          
            /* 
              拼接 ok 报文,包含:
                1. 0 标志位,占一字节
                2. affected_rows
                3. last_insert_id
                4. server_status
                5. warning_count
                6. message
              建立连接的时候,一般只有 server_status 有内容,其他都是0或者空
            */
            net_send_ok
              
              /* 发送 ok 报文 */
              my_net_write
      

挑战码报文

挑战码是一段随机字符串,server 发送给client用于加密client输入的密码,client给server的回包中包含挑战码加密后的密码,从而避免了网络传输明文密码

static bool send_server_handshake_packet(MPVIO_EXT *mpvio,
                                         const char *data, uint data_len)
{
  /* 申请 1 + 60 + 20 + 64 = 145 byte */
  char *buff= (char *) my_alloca(1 + SERVER_VERSION_LENGTH + data_len + 64);
  char scramble_buf[SCRAMBLE_LENGTH];
  /* end 是写报文的指针*/
  char *end= buff; 

  /* 报文第一位写 protocol_version */
  *end++= protocol_version;
  
  /* 
    第二位开始写 server_version
    笔者编译的时候带了suffix,version一共24字节,以5.6.16开头
    加'/0'一共25字节
  */
  end= strnmov(end, server_version, SERVER_VERSION_LENGTH) + 1;
  
  /* 4个字节保存 thread_id */
  int4store((uchar*) end, mpvio->thread_id);
  end+= 4;
  
  /* 
    发送头8个字节挑战码,用于兼容旧版客户端
    后12个字节写在后面,会被旧版客户端忽略
  */ 
  end= (char*) memcpy(end, data, SCRAMBLE_LENGTH_323);
  end+= SCRAMBLE_LENGTH_323;
  /* 第一段挑战码结尾 */
  *end++= 0;
  
  /*
    保存低16位能力位
    能力位作用见官方文档:
    https://dev.mysql.com/doc/internals/en/capability-flags.html
  */
  int2store(end, mpvio->client_capabilities);
  /* charset 信息 */
  end[2]= (char) default_charset_info->number;
  /* 
    server_status
    server_status 是 automatic, in trans 等状态信息
    每个位的含义在 include/mysql_com.h 中,以 SERVER_STATUS_ 开头的宏定义
  */                                                                                                                                                     
  int2store(end + 3, mpvio->server_status[0]);
  /* 能力位的高16位 */
  int2store(end + 5, mpvio->client_capabilities >> 16);
  /* 挑战码长度 */
  end[7]= data_len;
  DBUG_EXECUTE_IF("poison_srv_handshake_scramble_len", end[7]= -100;);
  /* 10字节0,应该是保留位 */
  memset(end + 8, 0, 10);
  /* 高低共4字节能力位,1字节charset,2字节server_status,1字节挑战码长度,10字节0,共18位 */
  end+= 18;
  /* 其余部分的挑战码 */
  end= (char*) memcpy(end, data + SCRAMBLE_LENGTH_323,
                      data_len - SCRAMBLE_LENGTH_323);
  end+= data_len - SCRAMBLE_LENGTH_323;
  /* auth plugin name,一般是 mysql_native_password,21字节 */
  end= strmake(end, plugin_name(mpvio->plugin)->str,
                    plugin_name(mpvio->plugin)->length);