分析 `MySQL` 的`网络`协议:`MySQL` 客户端与服务器之间的`握手`与`认证`过程。

MySQL 客户端与服务器握手与认证协议详解

大家好,今天我们来深入探讨 MySQL 客户端与服务器之间的握手与认证过程。这是客户端连接 MySQL 服务器的第一个也是至关重要的步骤,理解这个过程有助于我们更好地进行 MySQL 开发、调试以及安全性分析。

一、协议概述

MySQL 使用一种自定义的网络协议进行客户端与服务器之间的通信。这个协议基于 TCP/IP,并定义了一系列消息格式和交互流程。握手和认证是这个协议的初始阶段,其目的是:

  1. 服务器身份验证: 客户端需要确认连接的是合法的 MySQL 服务器,而不是一个恶意服务器。
  2. 客户端身份验证: 服务器需要验证客户端提供的用户名和密码是否正确,以确定客户端是否有权限访问数据库。
  3. 协议版本协商: 客户端和服务器需要协商使用哪个版本的协议,以确保双方能够正确地解析和处理消息。
  4. 能力协商: 客户端和服务器需要交换各自的能力信息,例如支持的字符集、认证插件等。

二、握手协议流程

握手协议主要包括以下几个步骤:

  1. 服务器发送初始握手包(Handshake Initialization Packet): 服务器在 TCP 连接建立后,立即向客户端发送一个初始握手包。这个包包含了服务器的版本号、连接 ID、认证所需的信息等。
  2. 客户端发送握手响应包(Handshake Response Packet): 客户端收到初始握手包后,根据服务器提供的信息,构造一个握手响应包发送给服务器。这个包包含了客户端的认证信息,例如用户名、加密后的密码等。
  3. 服务器发送认证结果包(Authentication Result Packet): 服务器收到握手响应包后,进行认证。如果认证成功,服务器发送一个 OK 包;如果认证失败,服务器发送一个 ERROR 包。
  4. 协议切换(可选): 如果客户端需要使用其他认证插件,或者需要升级协议版本,可能会进行协议切换。

下面我们来详细分析每个步骤的消息格式和内容。

三、初始握手包(Handshake Initialization Packet)详解

服务器发送的初始握手包包含了以下信息:

| 字段名称 | 长度 (字节) | 描述 ### 四、认证过程详解

接下来,我们以mysql_native_西服认证插件为例,详细说明客户端是如何进行认证的。

首先,服务器在初始握手包中会发送一个 默认的随机数,用于加密密码。客户端需要使用这个随机数对密码进行加密,然后将加密后的密码发送给服务器。

mysql_native_password插件的加密算法如下:

password = SHA1( SHA1( password ) XOR random_number )

其中:

  • SHA1 是 SHA1 散列算法。
  • password 是用户的原始密码。
  • random_number 是服务器发送的随机数。
  • XOR 是异或操作。

代码示例 (Python):

import hashlib

def mysql_native_password_hash(password, salt):
  """
  使用 mysql_native_password 插件的加密算法对密码进行加密。

  Args:
    password: 用户的原始密码 (字符串)。
    salt: 服务器发送的随机数 (字节串)。

  Returns:
    加密后的密码 (字节串)。
  """

  stage1 = hashlib.sha1(password.encode('utf-8')).digest()
  stage2 = hashlib.sha1(stage1).digest()
  combined = bytes(a ^ b for a, b in zip(stage2, salt[:20])) # 将 salt 截断为20字节,因为某些旧版本的 MySQL 只使用20字节的salt
  encrypted_password = hashlib.sha1(combined).digest()
  return encrypted_password

解释:

  1. password.encode('utf-8'): 将密码编码为 UTF-8 字节串。
  2. hashlib.sha1(...).digest(): 计算 SHA1 散列值,并返回字节串表示。
  3. bytes(a ^ b for a, b in zip(stage2, salt[:20])): 将 stage2 的 SHA1 散列值与 salt 进行异或操作。这里需要注意,salt 应该被截断为 20 字节,因为一些老的 MySQL 版本只使用了 20 字节的salt。
  4. hashlib.sha1(combined).digest(): 计算异或结果的 SHA1 散列值。

客户端在握手响应包中,将用户名和加密后的密码发送给服务器。服务器收到后,使用相同的算法对存储的密码进行加密,然后与客户端发送的密码进行比较。如果相同,则认证成功;否则,认证失败。

四、握手响应包(Handshake Response Packet)详解

握手响应包的格式取决于客户端支持的协议版本和能力。以下是 MySQL 4.1 及以上版本使用的握手响应包的格式:

字段名称 长度 (字节) 描述
client_flags 4 客户端能力标志。用于告知服务器客户端支持的功能。
max_packet_size 4 客户端可以接收的最大数据包大小。
character_set 1 客户端使用的字符集。
reserved 23 保留字段,必须全部填充为 0。
username 变长 用户名。以 NULL 结尾的字符串。
auth_response 变长 认证响应。例如,使用 mysql_native_password 插件时,包含加密后的密码。
database 变长 (可选) 初始数据库名称。如果指定,则在连接成功后自动切换到该数据库。以 NULL 结尾的字符串。
auth_plugin_name (仅在协议切换时存在) 变长 认证插件名称。如果客户端需要使用其他认证插件,则在此字段指定插件名称。以 NULL 结尾的字符串。
auth_data (仅在协议切换时存在) 变长 认证数据。用于认证插件特定的数据。

客户端能力标志 (client_flags):

client_flags 是一个重要的字段,它告知服务器客户端支持的功能。以下是一些常用的能力标志:

能力标志名称 描述
CLIENT_LONG_PASSWORD 0x00000001 使用旧的(pre-4.1)密码格式。
CLIENT_FOUND_ROWS 0x00000002 告知服务器返回匹配的行数,而不是受影响的行数。
CLIENT_LONG_FLAG 0x00000004 告知服务器客户端支持更大的结果集。
CLIENT_CONNECT_WITH_DB 0x00000008 客户端在连接时指定了数据库名称。
CLIENT_NO_SCHEMA 0x00000010 客户端不支持 db.table 语法。
CLIENT_COMPRESS 0x00000020 客户端支持压缩协议。
CLIENT_ODBC 0x00000040 客户端是 ODBC 客户端。
CLIENT_LOCAL_FILES 0x00000080 客户端允许 LOAD DATA LOCAL INFILE 语句。
CLIENT_IGNORE_SPACE 0x00000100 客户端允许在函数名之后使用空格。
CLIENT_PROTOCOL_41 0x00000200 客户端使用 4.1 协议。
CLIENT_INTERACTIVE 0x00000400 客户端是交互式客户端。
CLIENT_SSL 0x00000800 客户端支持 SSL 加密。
CLIENT_IGNORE_SIGPIPE 0x00001000 客户端忽略 SIGPIPE 信号。
CLIENT_TRANSACTIONS 0x00002000 客户端支持事务。
CLIENT_RESERVED 0x00004000 保留标志。
CLIENT_SECURE_CONNECTION 0x00008000 客户端支持安全的连接认证。
CLIENT_MULTI_STATEMENTS 0x00010000 客户端支持执行多个语句。
CLIENT_MULTI_RESULTS 0x00020000 客户端支持接收多个结果集。
CLIENT_PS_MULTI_RESULTS 0x00040000 客户端支持预处理语句的多个结果集。
CLIENT_PLUGIN_AUTH 0x00080000 客户端支持插件认证。
CLIENT_CONNECT_ATTRS 0x00100000 客户端支持连接属性。
CLIENT_PLUGIN_AUTH_LENENC 0x00200000 客户端支持长度编码的认证数据。
CLIENT_SSL_VERIFY_SERVER_CERT 0x00400000 客户端支持验证服务器 SSL 证书。
CLIENT_REMEMBER_OPTIONS 0x08000000 客户端应该记住选项。
CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS 0x10000000 客户端可以处理过期的密码。
CLIENT_SESSION_TRACK 0x80000000 客户端支持会话跟踪。
CLIENT_DEPRECATE_EOF 0x01000000 客户端不希望收到 EOF 数据包(在 5.7.5 之后)。

代码示例 (Python): 构建握手响应包

import struct

def create_handshake_response(username, password, salt, database=None):
  """
  创建握手响应包。

  Args:
    username: 用户名 (字符串)。
    password: 密码 (字符串)。
    salt: 服务器发送的随机数 (字节串)。
    database: 数据库名称 (字符串,可选)。

  Returns:
    握手响应包 (字节串)。
  """

  client_flags = (
      0  # 初始化为 0
      | 0x00000200  # CLIENT_PROTOCOL_41
      | 0x00080000  # CLIENT_PLUGIN_AUTH
      | 0x00008000  # CLIENT_SECURE_CONNECTION
  )

  max_packet_size = 2**24 - 1 # 最大数据包大小

  character_set = 8  # utf8mb4 字符集

  reserved = b'x00' * 23

  username_bytes = username.encode('utf-8') + b'x00'

  auth_response = mysql_native_password_hash(password, salt)
  auth_response_len = len(auth_response)
  auth_response_len_bytes = struct.pack('B', auth_response_len) # Length-encoded string

  database_bytes = (database.encode('utf-8') + b'x00') if database else b''

  packet = (
      struct.pack('<I', client_flags)
      + struct.pack('<I', max_packet_size)
      + struct.pack('B', character_set)
      + reserved
      + username_bytes
      + auth_response_len_bytes
      + auth_response
      + database_bytes
  )

  return packet

解释:

  1. client_flags: 设置客户端能力标志。这里我们设置了 CLIENT_PROTOCOL_41 (使用 4.1 协议), CLIENT_PLUGIN_AUTH (支持插件认证) 和 CLIENT_SECURE_CONNECTION (支持安全连接)。 可以根据自己的需要设置不同的标志。
  2. max_packet_size: 设置客户端可以接收的最大数据包大小。
  3. character_set: 设置客户端使用的字符集。
  4. reserved: 填充保留字段。
  5. username_bytes: 将用户名编码为 UTF-8 字节串,并添加 NULL 结尾符。
  6. auth_response: 使用 mysql_native_password_hash 函数对密码进行加密。
  7. database_bytes: 如果指定了数据库名称,则将其编码为 UTF-8 字节串,并添加 NULL 结尾符。
  8. struct.pack('<I', ...): 将整数打包为小端字节序的 4 字节整数。
  9. struct.pack('B', ...): 将整数打包为 1 字节整数。
  10. auth_response_len_bytes: 使用长度编码的字符串发送密码,因为MySQL 8.0.0之后要求使用长度编码。

五、认证结果包(Authentication Result Packet)详解

服务器收到握手响应包后,进行认证。认证结果通过一个数据包发送给客户端。这个数据包可能是以下两种类型之一:

  1. OK 包: 表示认证成功。
  2. ERROR 包: 表示认证失败。

OK 包:

OK 包的格式如下:

字段名称 长度 (字节) 描述
header 1 固定值为 0x00。
affected_rows 变长 受影响的行数。
last_insert_id 变长 最后一个插入的 ID。
status_flags 2 服务器状态标志。
warnings 2 警告数量。
info 变长 信息字符串。
session_state_changes 变长 会话状态变更信息,只有在CLIENT_SESSION_TRACK 被设置的情况下才存在。

ERROR 包:

ERROR 包的格式如下:

字段名称 长度 (字节) 描述
header 1 固定值为 0xFF。
error_code 2 错误代码。
sql_state 5 SQL 状态代码。
error_message 变长 错误信息。

代码示例 (Python): 解析 ERROR 包

import struct

def parse_error_packet(data):
  """
  解析 ERROR 包。

  Args:
    data: ERROR 包 (字节串)。

  Returns:
    包含错误代码、SQL 状态代码和错误信息的字典。
  """

  header = data[0]
  if header != 0xFF:
    raise ValueError("Not an ERROR packet")

  error_code = struct.unpack('<H', data[1:3])[0]
  sql_state = data[3:8].decode('utf-8')
  error_message = data[8:].decode('utf-8')

  return {
      "error_code": error_code,
      "sql_state": sql_state,
      "error_message": error_message,
  }

六、协议切换(Protocol Switching)

在某些情况下,客户端可能需要使用其他认证插件,或者需要升级协议版本。这时,客户端和服务器会进行协议切换。协议切换的过程如下:

  1. 客户端在握手响应包中,指定 auth_plugin_name 字段,告知服务器需要使用的认证插件。
  2. 服务器根据 auth_plugin_name 字段,加载相应的认证插件。
  3. 服务器可能会发送一个数据包给客户端,要求客户端提供认证插件特定的数据。
  4. 客户端根据服务器的要求,构造一个数据包发送给服务器。
  5. 服务器使用认证插件进行认证。

例如,MySQL 8.0 默认使用 caching_sha2_password 认证插件。如果客户端不支持该插件,可以切换到 mysql_native_password 插件。

七、代码示例:完整的握手过程模拟

为了更好地理解握手过程,我们可以使用 Python 模拟一个简单的 MySQL 客户端,与 MySQL 服务器进行握手。


import socket
import struct

def mysql_connect(host, port, username, password, database=None):
    """
    连接 MySQL 服务器并进行握手认证。

    Args:
        host: 服务器主机名。
        port: 服务器端口号。
        username: 用户名。
        password: 密码。
        database: 数据库名称 (可选)。

    Returns:
        socket 对象,如果连接成功。
        None, 如果连接失败。
    """

    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((host, port))

        # 1. 接收服务器初始握手包
        handshake_packet = sock.recv(1024) # 接收最大1024字节,理论上足够了
        if not handshake_packet:
            print("Error: Failed to receive handshake packet.")
            sock.close()
            return None

        # 解析握手包的前面几个字节确认协议版本
        protocol_version = handshake_packet[0]
        if protocol_version != 10: # MySQL 5.7 和 8.0 使用协议版本 10
            print(f"Error: Unsupported protocol version: {protocol_version}")
            sock.close()
            return None

        server_version_end = handshake_packet[1:].find(b'x00') + 1 # 从索引1开始找,因为索引0是protocol version
        server_version = handshake_packet[1:server_version_end].decode('utf-8')
        print(f"Server Version: {server_version}")

        salt_start = server_version_end + 9 # 跳过 connection_id (4 bytes), 2个reserved bytes, 然后是 auth-plugin-data-part-1 (8 bytes)
        salt_end = salt_start + 8
        salt = handshake_packet[salt_start:salt_end] # 8 字节 salt

        # 判断是否支持 CLIENT_PLUGIN_AUTH,如果支持,则读取剩余的salt
        capability_flags = struct.unpack('<H', handshake_packet[4:6])[0]
        if capability_flags & 0x0008:  

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注