本文内容翻译自:MySQL8.0.21官方文档
概述
MySQL通讯协议用于MySQL客户端和服务端的通讯,如下几个场景实现了MySQL通讯协议
连接客户端(Connector/C、Connector/J等)
MySQL代理
主从复制通讯部分
MySQL通讯协议支持以下特征:
通过SSL进行透明加密传输
透明的压缩传输
在连接阶段中能力集和身份方法协商
在命令阶段接收客户端命令并执行
协议基础
基本数据类型
整数类型
整数类型有两种形式的编码方式
Protocol::FixedLengthInteger
固定长度的整形类型,使用最低有效位在前的字节串方式存储的整数类型,也就是LITTLE-ENDIAN方式存储。固定长度整形有如下几种:
int<1> 1byte
int<2)> 2bytes
Int<3> 3bytes
int<4> 4bytes
int<6> 6bytes
int<8> 8bytes
Protocol::LengthEncodedInteger
根据要存储的数据大小,采用不同的长度来存储的整形类型,遵循如下规则:
小于251,用1字节标识
252~216,用0xFC + 2字节表示,如fc 39 01表示十进制313
216~224,用0xFD+3字节表示
224~264,用0xFE+8字节表示
字符串类型
有5种类型的字符串类型
固定长度字符串
带结束符0x00的字符串
可变长度字符串
带有可变长度整数前缀的字符串
包剩余字符串
MySQL协议包
MySQL客户端要发送数据,遵循以下规则:
把数据拆分成224字节大小的包
每个包添加一个报文头
Protocol::Packet
每个包的数据格式如下:
| 名称 | 类型 | 描述 |
|---|---|---|
| payload_length | int<3> | 报文体的长度,除前4字节之外的包长度 |
| sequence_id | int<1> | 序列号 |
| payload | string<payload_length> | 报文体 |
举例如下:
01 00 00 00 05 代表报文体长度01,sequence_id为0,报文体长度为1,数据为0x05发送超过16M的数据
如果报文体的长度等于或超过224-1,就要分包发送,第一个包的长度为224-1(0xFFFFFF),剩下的数据放到后续的包中,直至最后一个包的长度小于224-1。
sequence_id
sequence_id可以认为是包序号,每发一个包就+1,并自动循环,它从0开始增长,并当进入命令阶段时重置为0.
登录过程
MySQL登录协议是一个有状态的协议,当连接建立起来时,服务端初始化连接阶段
,连接阶段执行完成后,连接进入命令阶段
,命令阶段结束意味着连接断开。当有复制命令执行时,连接会进入复制阶段
。
连接过程
连接过程主要完成3个工作:
服务端和客户端能力协商
根据需要创建SSL通讯信道
对客户端进行身份验证
连接过程从客户端发起连接开始,服务器根据情况返回错误包
或者握手包
给客户端,客户端收到握手包
回应一个握手应答包
,在这个阶段,客户端可以请求SSL通讯,这种情况下,SSL通讯链路会在发送验证数据前建立完毕。在握手之后,服务端把身份过程需要的方法通知到客户端,并继续进行身份验证过程,直到返回成功应答OK_Package
或发ERR_Package
拒绝身份验证。
初始握手
初始握手时,服务端先发送Protocol::HandShake
包,客户端收到后,可选通过Protocol::SSLRequest
来创建SSL通讯信道,然后发送Protocol::HandShakeResponse
应答包。
能力协商
为了允许老客户端连接新的服务端,Protocol::HandShake
包携带了服务器版本和服务器能力标识,客户端需要在Protocol::HandShakeResponse
包中声明与服务端相同的能力。他们可以在status flags,SQL status for error codes,authentication methods,SSL Support,Compression
等方面达成共识。
确定身份验证方法
身份验证方法是跟用户账户联系在一起的,保存在mysql.user
表中的plugin
列中,客户端在Protocol::HandShakeResponse
包中声明他想使用的方法,服务端查询mysql.user
表来决定要使用的身份验证方法。为了减少交互次数,服务端和客户端已经在初始握手中用乐观猜测的方式进行了协商。服务端用他的默认身份验证方法产生初始的身份验证数据,和身份验证方法名一起,通过Protocol::HandShake
包发给客户端。客户端可以用包含Protocol::HandShakeResponse
包对服务器发来的身份验证数据进行应答。客户端没有义务一定要使用服务端发来的身份验证方法,而是把它要使用的方法放在Protocol::HandShakeResponse
中,如果两者不一致,服务端会用Protocol::AuthSwitchRequest
通知客户端更换身份验证方法。
如果客户端或服务端不支持可插拔的身份验证方法,选择要采用的身份验证方法根据服务端和客户端的能力标识采用如下策略,如果
CLIENT_PROTOCOL_41
或 CLIENT_SECURE_CONNECTION
没有设置,采用Old Password authentication
,如果
CLIENT_PROTOCOL_41
和 CLIENT_SECURE_CONNECTION
都设置了,而CLIENT_PLUGIN_AUTH
没有设置,则采用Native Authentication
。
认证阶段快速路径
假定客户端要用使用server_method
身份验证方法的用户U
进行登录,在满足如下条件时,快速路径会生效:
服务端使用
server_method
生成验证数据,通过Protocol::HandShake
发给客户端客户端使用与服务端使用的
server_method
兼容的client_authentication_method
放在Protocol::HandShakeResponse
应答包中。
这种情况下,第一阶段的身份验证在初始握手阶段已经开始了,接下来,根据服务端验证方法,可以继续进行数据交互,直至通过验证或拒绝验证。
成功认证
一个成功的快速认证过程如下:
客户端发起网络连接
服务端向客户端发
Protocol::HandShake客户端向服务端应答
Protocol::HandShakeResponse客户端和服务端进行服务端认证需要的其他数据交互
服务端应答成功认证消息
OK_Package客户端和服务端进入命名阶段。
在第四步的交互中,服务器向客户端发送Protocol::AuthMoreData
包,这个包有一个0x01
的前缀以区别于ERR_Package
和OK_Package
。
失败认证
与成功认证类似,但是最终服务端决定不允许客户端认证通过,就返回一个ERR_Package
而不是OK_Package
。
认证方法不匹配
假设客户端使用认证方式为M的用户U尝试登陆,如果有如下情况之一:
服务端默认的用于生成
Protocol::HandShake
中身份认证负载数据的默认方法不是M客户端用于生成
Protocol::HandShakeResponse
中身份认证应答数据的方法与M不兼容
这就是身份验证方法不匹配,必须使用正确的身份验证方法重启身份验证过程。如果发生了认证方法不匹配,服务端会向客户端发送Protocol::AuthSwitchRequest
,包含客户端需要使用的身份验证方法和用这个方法生成的身份验证数据,客户端需要用这个方法来继续进行身份验证交互操作,如果客户端不能识别新的方法,就主动断开连接。
认证方法改变
如果发生了认证方法不匹配,就会发生认证方法改变,流程如下:
客户端发起网络连接
服务端发起
Protocol::HandShake客户端回应
Protocol::HandShakeResponse服务端发送
Protocol::AuthSwitchRequest
告诉客户端需要用新的身份验证方法客户端和服务端按照新的身份验证方法需要的数据继续进行交互
服务端根据验证结果返回
ERR_Package
或OK_Package
客户端能力不足
如果服务端发现客户端能力不足以完成身份验证,会应答ERR_Package
拒绝连接,存在如下可能性:
不支持可插拔身份验证方法的客户端(
CLIENT_PLUGIN_AUTH
没有设置)连接到一个使用非Native authentication
的账户不支持安全身份身份认证的客户端(
CLIENT_SECURE_CONNECTION
没有设置)服务端用于生成身份验证数据的身份验证方法与
Native authentication
不兼容,且客户端不支持可插拔的身份验证方法(CLIENT_PLUGIN_AUTH
没有设置)
以上这些情况,在收到客户端的Protocol::HandShakeResponse
之后,服务端会应答ERR_Package
拒绝连接。
客户端无法识别的身份验证方法
即使客户端支持可插拔的身份验证方法,但可能存在不能识别服务端在Protocol::AuthSwitchRequest
中要求客户端使用的身份验证方法,这种情况下,客户端直接断开连接。
Non-CLIENT_PLUGIN_AUTH客户端
只有在如下情况下,服务端才会要求不支持可插拔身份验证方法的客户端改变身份验证方法:
客户端在
Protocol::HandShakeResponse
中表明使用Native authentication客户端支持安全身份验证方法
服务端的默认身份验证方法是
Native authentication
这种情况下,服务端发送Protocol::OldAuthSwitchRequest
,这个包不会包含新的身份验证方法和数据,因为它隐含假定使用Native authentication
方法。客户端会复用Protocol::HandShake
中的随机字符来生成密码哈希,然后应答Protocol::HandShakeResponse320
包。
COM_CHANGE_USER命令后的身份验证
在命令阶段,客户端如果使用了COM_CHANGE_USER命令,就会触发一个完整的身份验证过程。同连接阶段类似,服务端会对普通的快速路径认证或通过Protocol::AuthSwitchRequest
进行的认证回应OK_Package
或ERR_Package
,在这个过程中,新的身份验证方法所需的交互正常进行,并最终接受或拒绝认证。
COM_CHANGE_USER和Non-CLIENT_PLUGIN_AUTH客户端
不支持可插拔身份验证的客户端可以对使用Native authentication
或Old Password authentication
的账户发送COM_CHANGE_USER命令。这种情况下,它假定服务端已经发送了身份验证所需的数据(也就是Protocol::HandShake
包中携带的数据),客户端回应这个命令(比如发送密码哈希值)。
登录阶段数据包
Protocol::HandShake
从3.0.21版本开始,统一使用Protocol::HandShakeV10
,报文体内容如下:
| 名称 | 类型 | 描述 | 条件 |
|---|---|---|---|
| protocol_version | int<1> | 固定为10(0x0A) | |
| server_version | string<NUL> | 可读字符服务器版本,如8.0.21 | |
| thread_id | int<4> | 连接id,会话列表中的ID | |
| auth_plugin_data_1 | String[8] | 认证数据第一部分 | |
| filter | int<1> | 0x00 | |
| capability_flags_1 | int<2> | 服务端能力的低2字节 | |
| character_set | int<1> | 服务端默认字符集 | |
| status_flag | int<2> | SERVER_STATUS_flags_enum | |
| capability_flags_2 | int<2> | 服务端能力高字节 | |
| auth_plugin_data_len | int<1> | 验证所需数据长度 | if CLIENT_PLUGIN_AUTH |
| 0x00 | int<1> | 0x00 | else |
| reserved | String[10] | 保留使用,全部00 | |
| auth-plugin-data-part-2 | string[length] | 身份验证所需数据的剩余部分,长度为 max(13,auth_plugin_data_len - 8) | if CLIENT_PLUGIN_AUTH |
| auth_plugin_name | string<NUL> | 身份验证方法名 |
Protocol::HandshakeResponse
根据服务端能力是否支持CLIENT_PROTOCOL_41
以及客户端是否能识别CLIENT_PROTOCOL_41
来决定是使用Protocol::HandShakeResponse320
或Protocol::HandShakeResponse410
。这里仅列出Protocol::HandShakeResponse410
| 名称 | 类型 | 描述 | 条件 |
|---|---|---|---|
| client_flag | int<4> | 客户端能力集 | |
| max_packet_size | int<4> | 最大包长度 | |
| character_set | int<1> | 客户端字符集 | |
| filler | String[23] | 目前全部00 | |
| username | string<NUL> | 登录用户名 | |
| auth_response | string<length> | 身份验证方法生成的不透明的身份验证应答数据 | 设置了 CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA |
| auth_response_length | int<1> | auth_response长度 | 没设置CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA |
| auth_response | string<length> | auth_response | 没设置CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA |
| database | string<NUL> | 连接的初始数据库 | 设置了 CLIENT_CONNECT_WITH_DB |
| client_plugin_name | string<NUL> | 客户端使用的身份验证方法 | 设置了 CLIENT_PLUGIN_AUTH |
| length of attrs | int<lenenc> | 连接属性的总长度 | 设置了 CLIENT_CONNECT_ATTRS |
| Key1 | string<lenenc> | 第一个键 | 设置了 CLIENT_CONNECT_ATTRS |
| Value1 | string<lenenc> | 第一个值 | 设置了 CLIENT_CONNECT_ATTRS |
| ... | ... | 后续的键值对,如果有更多的话 | 设置了 CLIENT_CONNECT_ATTRS |
| zstd_compression_level | int<1> | zstd压缩算法的压缩级别 |
根据客户端的能力集,返回结果不同,根据实际情况解析。
Protocol::AuthSwitchRequest
如果客户端和服务端均支持CLIENT_PLUGIN_AUTH
,服务端可能会发送这个报文,要求客户端采用另一种身份验证方法,一般发生在Protocol::HandShakeReponse
中的身份验证方法和Protocol::HandShake
中的身份验证方法名不一致时。
| 名称 | 类型 | 描述 |
|---|---|---|
| status_tag | int<1> | 固定0xFE |
| plugin name | string<NUL> | 要求客户端使用的身份验证方法名 |
| plugin provided data | string<EOF> | 客户端需要使用的初始化身份验证数据 |
Protocol::AuthSwitchResponse
这个包包含了客户端需要服务器校验的身份认证数据,对协议是不透明的。
| 名称 | 类型 | 描述 |
|---|---|---|
| data | string<EOF> | 身份验证应答数据 |
Protocol::AuthMoreData
为了确保在给客户端发送一些身份验证插件需要的数据时,不会被客户端认为是“带外”命令数据,就需要把这些数据用0x01包装一下,客户端正常发送Protocol::AuthSwitchResponse
即可。
| 名称 | 类型 | 描述 |
|---|---|---|
| status_tag | int<1> | 固定0x01 |
| authentication method data | string<EOF> | 客户端需要使用的其他身份验证数据 |
Protocol::OldAuthSwitchRequest Protocol::SSLRequest
留待后续研究
身份验证方法
为了满足服务端的身份验证要求,客户端使用几个身份验证方法的一种,从mysql5.5开始,用于验证连接身份的验证方法保存在mysql.user表的plugin字段,在之前的版本中,只能用MYSQL_SECURE_CONNECTION标志标识的mysql native authentication
或old password authentication
。后者已经基本不用了,不再赘述。
每种身份验证方法包含一个服务端插件名、一个客户端插件名和一个特定的交互方式,方法协商的输入输出数据一般通过Protocol::HandShake
,Protocol::HandShakeRespnse
以及Protocol::AuthSwitchRequest
和它后续的包,数据结构通常都是一样的。
一些限制
虽然身份验证方法的协商是自由形式的,但为了不增加额外的往返交互,还是存在一些限制条件的。
Protocol::HandShake
携带的auth_plugin_data最大长度为255字节如果
CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA
没有设置,则Protocol::HandShakeResponse
携带的auth_reponse_data最大长度为255字节客户端侧插件可能无法在初始握手中得到需要的数据
Native Authentication
原生的身份验证方法Authentication::Native41
服务端插件名
mysql_native_password客户端插件名
*mysql_native_password客户端侧需要20字节的随机字符串
客户端侧基于下面描述的算法,发送20字节的应答数据。
这个验证方法解决了Old Password authentication
的两个缺陷
使用了更安全的SHA1哈希算法
仅知道mysql.user表中的密码密文,无法进行身份验证,安全性有所提高。
密码算法如下:
客户端在Protocol::HandShakeResponse
或Protocol::AuthSwitchResponse
中发送的密文数据算法如下SHA1(password) xor SHA1("20-bytes random data from server" <concat> SHA1( SHA1( password ) ) )
服务端保存在mysql.user表里authentication_string值为SHA1( SHA1( password) ),服务端知道它发送的随机数据,可以按照客户端算法重新计算,然后与客户端发送的数据对比,一致则验证通过,否则拒绝验证。
Caching_sha2_password information
新版MySQL默认的身份验证方法,在Protocol::HandShake
包中会被返回。定义如下:
服务端侧插件名称
caching_sha2_password客户端侧插件名称
caching_sha2_password账号——用户名和机器名组合
authentication_string——存储在mysql.user表中转换后的用户密码
user_password——需要知道的特定用户密码用于生成authentication_string
client_password——客户端连接时需要知道的密码
Nonce——20字节随机数
Scramble——XOR(SHA256(password), SHA256(SHA256(SHA256(password)), Nonce))
Hash entry——account_name -> SHA256(SHA256(user_password))
插件工作原理
存在两种工作流程:
快速认证流程
完整认证流程
如果服务端已经缓存了登录用户的Hash Entry
,就用客户端发送的Scramble
来进行快速认证,如果校验成功,则身份认证完成,客户端连接进入命令模式。如果校验失败,服务端会通知客户端切换到完整认证流程,这个流程需要加密信道上传输密码(也就是说完整认证流程必须在SSL模式下才能进行),服务端把客户端密码与authentication_string
进行对比,如果成功,服务端就缓存当前用户的Hash Entry
,客户端连接进入命令模式,如果失败,服务端返回错误应答,通知客户端断开连接。




