MySQL 客户端与服务器握手与认证协议详解
大家好,今天我们来深入探讨 MySQL 客户端与服务器之间的握手与认证过程。这是客户端连接 MySQL 服务器的第一个也是至关重要的步骤,理解这个过程有助于我们更好地进行 MySQL 开发、调试以及安全性分析。
一、协议概述
MySQL 使用一种自定义的网络协议进行客户端与服务器之间的通信。这个协议基于 TCP/IP,并定义了一系列消息格式和交互流程。握手和认证是这个协议的初始阶段,其目的是:
- 服务器身份验证: 客户端需要确认连接的是合法的 MySQL 服务器,而不是一个恶意服务器。
- 客户端身份验证: 服务器需要验证客户端提供的用户名和密码是否正确,以确定客户端是否有权限访问数据库。
- 协议版本协商: 客户端和服务器需要协商使用哪个版本的协议,以确保双方能够正确地解析和处理消息。
- 能力协商: 客户端和服务器需要交换各自的能力信息,例如支持的字符集、认证插件等。
二、握手协议流程
握手协议主要包括以下几个步骤:
- 服务器发送初始握手包(Handshake Initialization Packet): 服务器在 TCP 连接建立后,立即向客户端发送一个初始握手包。这个包包含了服务器的版本号、连接 ID、认证所需的信息等。
- 客户端发送握手响应包(Handshake Response Packet): 客户端收到初始握手包后,根据服务器提供的信息,构造一个握手响应包发送给服务器。这个包包含了客户端的认证信息,例如用户名、加密后的密码等。
- 服务器发送认证结果包(Authentication Result Packet): 服务器收到握手响应包后,进行认证。如果认证成功,服务器发送一个 OK 包;如果认证失败,服务器发送一个 ERROR 包。
- 协议切换(可选): 如果客户端需要使用其他认证插件,或者需要升级协议版本,可能会进行协议切换。
下面我们来详细分析每个步骤的消息格式和内容。
三、初始握手包(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
解释:
password.encode('utf-8'): 将密码编码为 UTF-8 字节串。hashlib.sha1(...).digest(): 计算 SHA1 散列值,并返回字节串表示。bytes(a ^ b for a, b in zip(stage2, salt[:20])): 将 stage2 的 SHA1 散列值与 salt 进行异或操作。这里需要注意,salt应该被截断为 20 字节,因为一些老的 MySQL 版本只使用了 20 字节的salt。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
解释:
client_flags: 设置客户端能力标志。这里我们设置了CLIENT_PROTOCOL_41(使用 4.1 协议),CLIENT_PLUGIN_AUTH(支持插件认证) 和CLIENT_SECURE_CONNECTION(支持安全连接)。 可以根据自己的需要设置不同的标志。max_packet_size: 设置客户端可以接收的最大数据包大小。character_set: 设置客户端使用的字符集。reserved: 填充保留字段。username_bytes: 将用户名编码为 UTF-8 字节串,并添加 NULL 结尾符。auth_response: 使用mysql_native_password_hash函数对密码进行加密。database_bytes: 如果指定了数据库名称,则将其编码为 UTF-8 字节串,并添加 NULL 结尾符。struct.pack('<I', ...): 将整数打包为小端字节序的 4 字节整数。struct.pack('B', ...): 将整数打包为 1 字节整数。auth_response_len_bytes: 使用长度编码的字符串发送密码,因为MySQL 8.0.0之后要求使用长度编码。
五、认证结果包(Authentication Result Packet)详解
服务器收到握手响应包后,进行认证。认证结果通过一个数据包发送给客户端。这个数据包可能是以下两种类型之一:
- OK 包: 表示认证成功。
- 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)
在某些情况下,客户端可能需要使用其他认证插件,或者需要升级协议版本。这时,客户端和服务器会进行协议切换。协议切换的过程如下:
- 客户端在握手响应包中,指定
auth_plugin_name字段,告知服务器需要使用的认证插件。 - 服务器根据
auth_plugin_name字段,加载相应的认证插件。 - 服务器可能会发送一个数据包给客户端,要求客户端提供认证插件特定的数据。
- 客户端根据服务器的要求,构造一个数据包发送给服务器。
- 服务器使用认证插件进行认证。
例如,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: