MySQL协议分析之握手认证

介绍

每次使用MySQL的时候,都是直接使用编写好的驱动,只关注业务部分。此次想探索一下链接的过程,所以有了此次总结。html

与MySQL服务器的交互,能够分为四个阶段mysql

  1. 创建TCP链接
  2. 握手认证阶段
  3. 命令执行阶段
  4. 断开链接

这里主要探索握手认证的阶段,注意这里的握手认证,和TCP的三次握手不是同一个,是先创建了TCP链接,即已经完成了TCP三次握手,才进入到MySQL的握手认证。git

MySQL数据报文固定格式

MySQL的数据报文,由固定4byte的消息头消息体组成。 其中消息头的前3个byte表明消息体的实际大小,第4个byte表明序号,用于保证消息顺序的正确。github

解析握手包

当客户端与MySQL服务器创建起TCP链接的时候,MySQL服务器会发送一个握手包给客户端,称为HandshakeV10,来分析一下这个报文结构:sql

大小(单位byte) 名称 解释
1 protocol version 协议版本号
n server version MySQL服务器版本,string[NUL]类型
4 connection id 服务器线程ID
8 auth-plugin-data-part-1 挑战随机数第一部分
1 filler 1byte的填充数,值为0x00
2 capability flags 服务器权能标志,低16位
1 character set 字符编码
2 status flags 服务器状态
2 capability flags 服务器权能标志,高16位
1 length of auth-plugin-data 挑战数长度
10 reserved 10byte的填充数,值为0x00
13 auth-plugin-data-part-2 挑战随机数第二部分,最少12字节,最长13字节
n auth-plugin name 验证插件的名称,string[NUL]类型

string[NUL] 以0x00结尾的字符串 数据库

首先MySQL采用的是挑战/应答的模式进行验证,所以获取到这个握手包以后,要对该报文进行解析,获取到挑战随机数,用于下个步骤中进行应答。服务器

定义一个结构体 HandsharkProtocol数据结构

type HandsharkProtocol struct {
	ProtocolVersion     byte
	ServerVersion       string
	ConnectionId        uint32
	AuthPluginDataPart1 []byte
	Filler              byte
	CapabilityFlag1     []byte
	CharacterSet        byte
	StatusFlags         []byte
	CapabilityFlag2     []byte
	AuthPluginDataLen   byte
	Reserved            []byte
	AuthPluginDataPart2 []byte
	AuthPluginName      string
}
复制代码

解析握手包的过程,就按照定义,逐个解析app

func DecodeHandshark(b []byte) *HandsharkProtocol {
	hs := HandsharkProtocol{}
	buff := bytes.NewBuffer(b) // 将[]byte转成buffer,方便处理
	buff.Next(4) // 前四个字节为消息头,不处理
	hs.ProtocolVersion, _ = buff.ReadByte() // 协议版本号
	hs.ServerVersion, _ = buff.ReadString(0x00) // MySQL服务器版本 0x00结尾的字符串
	hs.ConnectionId = binary.LittleEndian.Uint32(buff.Next(4)) // 服务器线程ID 小端法存储
	hs.AuthPluginDataPart1 = buff.Next(8) // 挑战随机数第一部分
	hs.Filler, _ = buff.ReadByte() // 1byte的填充
	hs.CapabilityFlag1 = buff.Next(2) // 服务器权能标志 低16位

	if buff.Len() > 0 { // if more data in the packet
		hs.CharacterSet, _ = buff.ReadByte() // 字符编码
		hs.StatusFlags = buff.Next(2) // 服务器状态
		hs.CapabilityFlag2 = buff.Next(2) // 服务器权能标志 高16位
		hs.AuthPluginDataLen, _ = buff.ReadByte() // 挑战数长度
		hs.Reserved = buff.Next(10) // 10 byte的填充
		hs.AuthPluginDataPart2 = buff.Next(12) // 挑战随机数第二部分
		buff.ReadByte() // 挑战随机数第13个byte,0x00 所以无视
		hs.AuthPluginName, _ = buff.ReadString(0x00) // 验证插件的名称 0x00结尾的字符串
	}

	return &hs
}
复制代码

来测试一下,解析出来的握手包结构tcp

func main() {
	c, err := net.Dial("tcp", ":3306")
	if err != nil {
		fmt.Println(err)
	}
	pkg := make([]byte, 1024)
	_, _ = c.Read(pkg)
	h := DecodeHandshark(pkg)
	fmt.Println(h)
}
复制代码

h 为 &{10 8.0.19 421 [56 49 17 70 10 105 114 72] 0 [255 255] 255 [2 0] [255 199] 21 [0 0 0 0 0 0 0 0 0 0] [24 87 2 88 38 4 19 40 89 59 84 62] caching_sha2_password},协议版本为10,服务器版本8.0.19等等信息均可以直观的看到,接下来就是进行验证应答。

发送握手包响应数据

首先看看响应握手包的数据结构HandshakeResponse41

大小(单位byte) 名称 解释
4 capability flags 客户端全能标志位,通常值为CLIENT_PROTOCOL_41(0x00000200)
4 max-packet size 包的最大长度,能够设置成0x00
1 character set 字符编码
23 reserved 23byte的填充数,值为0x00
n username 要登陆的用户名 string[NUL]型
1 length of auth-response 应答挑战数的长度
n auth-response 应答挑战数
n database 要链接的数据库名称 string[NUL]型
n auth plugin name 验证插件名称

因为采用的是caching_sha2_password验证的方式,所以根据该方式,hash数据库的登陆密码

// 这段代码是使用 github.com/go-sql-driver/mysql包中的方法
func scrambleSHA256Password(scramble []byte, password string) []byte {
	if len(password) == 0 {
		return nil
	}

	crypt := sha256.New()
	crypt.Write([]byte(password))
	message1 := crypt.Sum(nil)

	crypt.Reset()
	crypt.Write(message1)
	message1Hash := crypt.Sum(nil)

	crypt.Reset()
	crypt.Write(message1Hash)
	crypt.Write(scramble)
	message2 := crypt.Sum(nil)

	for i := range message1 {
		message1[i] ^= message2[i]
	}

	return message1
}
复制代码

有了这个基础以后,就能够开始构造报文了

func GetHandshakeResponsePacket(authResp []byte, plugin string) []byte {
	//消息体的长度
	authLen := len(authResp)
	pkgBodyLen := 4 + 4 + 1 + 23 + len(user) + 1 + authLen + len(authResp) + len(dbName) + 1 + len(plugin) + 1
	data := make([]byte, pkgBodyLen + 4) // + 4的消息头长度

	// capability flags
	var capabilityFlags uint32 = 512 | 8 | 524288 
	// 客户端权能标志位
	// 512 表明 clientProtocol41,
	// 8 表明 clientConnectWithDB, 
	// 524288 表明 clientPluginAuth
	// 使用小端法表示,参照 binary.LittleEndian.PutUint32() 方法
	data[4] = byte(capabilityFlags)
	data[5] = byte(capabilityFlags >> 8)
	data[6] = byte(capabilityFlags >> 16)
	data[7] = byte(capabilityFlags >> 24)

	// max-size
	data[8] = 0x00
	data[9] = 0x00
	data[10] = 0x00
	data[11] = 0x00

	// character set
	data[12] = 255 // 255 对应 utf-8

	// reserved
	pos := 13
	for ; pos < 13 + 23; pos++ {
		data[pos] = 0
	}

	//username
	pos += copy(data[pos:], user)
	data[pos] = 0x00
	pos++

	// authResp
	pos += copy(data[pos:], []byte{byte(uint64(authLen))})
	pos += copy(data[pos:], authResp)

	// dbName
	pos += copy(data[pos:], dbName)
	data[pos] = 0x00
	pos++

	// plugin
	pos += copy(data[pos:], plugin)
	data[pos] = 0x00
	pos++

	return data[:pos]
}
复制代码

最后再补上消息头,就能够发送给MySQL服务器

const user = "root" // 用户名
const password = "123" // 密码
const dbName = "test" // 数据库名

func main() {
	c, err := net.Dial("tcp", ":3306")
	if err != nil {
		fmt.Println(err)
	}
	pkg := make([]byte, 1024)
	_, _ = c.Read(pkg)
	h := DecodeHandshark(pkg)

        // 构造响应报文
	scramble := append(h.AuthPluginDataPart1, h.AuthPluginDataPart2...)
	authResp := scrambleSHA256Password(scramble, password)
	respPacket := GetHandshakeResponsePacket(authResp, h.AuthPluginName)
	pktLen := len(respPacket) - 4 // 减掉消息头长度
	respPacket[0] = byte(pktLen)
	respPacket[1] = byte(pktLen >> 8)
	respPacket[2] = byte(pktLen >> 16) // 前3个byte小端法表示消息体大小
	respPacket[3] = 1 // 序号
	c.Write(respPacket)
	
	// 这两句为了阻塞程序,方便查看是否已经链接上mysql
	ch := make(<-chan int, 1) 
	<-ch 
}
复制代码

确认认证结果,完成认证

最后来看看是否链接成功,在程序没有链接以前,mysql服务器里只有两个客户端链接

而后运行程序

能够看到多了一个客户端链接,说明握手认证成功,以后就能够发送命令给mysql执行。

总结

虽然在日常的工做中,不会本身编写链接驱动,可是秉着好奇的心,研究了一下mysql的链接过程,仍是受益颇多~。

Thanks!

相关文章
相关标签/搜索