暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL客户端/服务端通讯协议

读读书写写代码 2021-09-26
668

本文内容翻译自: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客户端要发送数据,遵循以下规则:

  1. 把数据拆分成224字节大小的包

  2. 每个包添加一个报文头

Protocol::Packet

每个包的数据格式如下:

名称类型描述
payload_lengthint<3>报文体的长度,除前4字节之外的包长度
sequence_idint<1>序列号
payloadstring<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
    应答包中。

这种情况下,第一阶段的身份验证在初始握手阶段已经开始了,接下来,根据服务端验证方法,可以继续进行数据交互,直至通过验证或拒绝验证。

成功认证

一个成功的快速认证过程如下:

  1. 客户端发起网络连接

  2. 服务端向客户端发Protocol::HandShake

  3. 客户端向服务端应答Protocol::HandShakeResponse

  4. 客户端和服务端进行服务端认证需要的其他数据交互

  5. 服务端应答成功认证消息OK_Package

  6. 客户端和服务端进入命名阶段。

在第四步的交互中,服务器向客户端发送Protocol::AuthMoreData
包,这个包有一个0x01
的前缀以区别于ERR_Package
OK_Package

失败认证

与成功认证类似,但是最终服务端决定不允许客户端认证通过,就返回一个ERR_Package
而不是OK_Package

认证方法不匹配

假设客户端使用认证方式为M的用户U尝试登陆,如果有如下情况之一:

  1. 服务端默认的用于生成Protocol::HandShake
    中身份认证负载数据的默认方法不是M

  2. 客户端用于生成Protocol::HandShakeResponse
    中身份认证应答数据的方法与M不兼容

这就是身份验证方法不匹配,必须使用正确的身份验证方法重启身份验证过程。如果发生了认证方法不匹配,服务端会向客户端发送Protocol::AuthSwitchRequest
,包含客户端需要使用的身份验证方法和用这个方法生成的身份验证数据,客户端需要用这个方法来继续进行身份验证交互操作,如果客户端不能识别新的方法,就主动断开连接。

认证方法改变

如果发生了认证方法不匹配,就会发生认证方法改变,流程如下:

  1. 客户端发起网络连接

  2. 服务端发起Protocol::HandShake

  3. 客户端回应Protocol::HandShakeResponse

  4. 服务端发送Protocol::AuthSwitchRequest
    告诉客户端需要用新的身份验证方法

  5. 客户端和服务端按照新的身份验证方法需要的数据继续进行交互

  6. 服务端根据验证结果返回ERR_Package
    OK_Package

客户端能力不足

如果服务端发现客户端能力不足以完成身份验证,会应答ERR_Package
拒绝连接,存在如下可能性:

  1. 不支持可插拔身份验证方法的客户端(CLIENT_PLUGIN_AUTH
    没有设置)连接到一个使用非Native authentication
    的账户

  2. 不支持安全身份身份认证的客户端(CLIENT_SECURE_CONNECTION
    没有设置)

  3. 服务端用于生成身份验证数据的身份验证方法与Native authentication
    不兼容,且客户端不支持可插拔的身份验证方法(CLIENT_PLUGIN_AUTH
    没有设置)

以上这些情况,在收到客户端的Protocol::HandShakeResponse
之后,服务端会应答ERR_Package
拒绝连接。

客户端无法识别的身份验证方法

即使客户端支持可插拔的身份验证方法,但可能存在不能识别服务端在Protocol::AuthSwitchRequest
中要求客户端使用的身份验证方法,这种情况下,客户端直接断开连接。

Non-CLIENT_PLUGIN_AUTH客户端

只有在如下情况下,服务端才会要求不支持可插拔身份验证方法的客户端改变身份验证方法:

  1. 客户端在Protocol::HandShakeResponse
    中表明使用Native authentication

  2. 客户端支持安全身份验证方法

  3. 服务端的默认身份验证方法是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_versionint<1>固定为10(0x0A)
server_versionstring<NUL>可读字符服务器版本,如8.0.21
thread_idint<4>连接id,会话列表中的ID
auth_plugin_data_1String[8]认证数据第一部分
filterint<1>0x00
capability_flags_1int<2>服务端能力的低2字节
character_setint<1>服务端默认字符集
status_flagint<2>SERVER_STATUS_flags_enum
capability_flags_2int<2>服务端能力高字节
auth_plugin_data_lenint<1>验证所需数据长度if CLIENT_PLUGIN_AUTH
0x00int<1>0x00else
reservedString[10]保留使用,全部00
auth-plugin-data-part-2string[length]身份验证所需数据的剩余部分,长度为 max(13,auth_plugin_data_len - 8)if CLIENT_PLUGIN_AUTH
auth_plugin_namestring<NUL>身份验证方法名

Protocol::HandshakeResponse

根据服务端能力是否支持CLIENT_PROTOCOL_41
以及客户端是否能识别CLIENT_PROTOCOL_41
来决定是使用Protocol::HandShakeResponse320
Protocol::HandShakeResponse410
。这里仅列出Protocol::HandShakeResponse410

名称类型描述条件
client_flagint<4>客户端能力集
max_packet_sizeint<4>最大包长度
character_setint<1>客户端字符集
fillerString[23]目前全部00
usernamestring<NUL>登录用户名
auth_responsestring<length>身份验证方法生成的不透明的身份验证应答数据设置了 CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA
auth_response_lengthint<1>auth_response长度没设置CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA
auth_responsestring<length>auth_response没设置CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA
databasestring<NUL>连接的初始数据库设置了 CLIENT_CONNECT_WITH_DB
client_plugin_namestring<NUL>客户端使用的身份验证方法设置了 CLIENT_PLUGIN_AUTH
length of attrsint<lenenc>连接属性的总长度设置了 CLIENT_CONNECT_ATTRS
Key1string<lenenc>第一个键设置了 CLIENT_CONNECT_ATTRS
Value1string<lenenc>第一个值设置了 CLIENT_CONNECT_ATTRS
......后续的键值对,如果有更多的话设置了 CLIENT_CONNECT_ATTRS
zstd_compression_levelint<1>zstd压缩算法的压缩级别

根据客户端的能力集,返回结果不同,根据实际情况解析。

Protocol::AuthSwitchRequest

如果客户端和服务端均支持CLIENT_PLUGIN_AUTH
,服务端可能会发送这个报文,要求客户端采用另一种身份验证方法,一般发生在Protocol::HandShakeReponse
中的身份验证方法和Protocol::HandShake
中的身份验证方法名不一致时。

名称类型描述
status_tagint<1>固定0xFE
plugin namestring<NUL>要求客户端使用的身份验证方法名
plugin provided datastring<EOF>客户端需要使用的初始化身份验证数据

Protocol::AuthSwitchResponse

这个包包含了客户端需要服务器校验的身份认证数据,对协议是不透明的。

名称类型描述
datastring<EOF>身份验证应答数据

Protocol::AuthMoreData

为了确保在给客户端发送一些身份验证插件需要的数据时,不会被客户端认为是“带外”命令数据,就需要把这些数据用0x01包装一下,客户端正常发送Protocol::AuthSwitchResponse
即可。

名称类型描述
status_tagint<1>固定0x01
authentication method datastring<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
的两个缺陷

  1. 使用了更安全的SHA1哈希算法

  2. 仅知道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))

插件工作原理

存在两种工作流程:

  1. 快速认证流程

  2. 完整认证流程

如果服务端已经缓存了登录用户的Hash Entry
,就用客户端发送的Scramble
来进行快速认证,如果校验成功,则身份认证完成,客户端连接进入命令模式。如果校验失败,服务端会通知客户端切换到完整认证流程,这个流程需要加密信道上传输密码(也就是说完整认证流程必须在SSL模式下才能进行),服务端把客户端密码与authentication_string
进行对比,如果成功,服务端就缓存当前用户的Hash Entry
,客户端连接进入命令模式,如果失败,服务端返回错误应答,通知客户端断开连接。




文章转载自读读书写写代码,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论