mysql握手关闭_MySQL协议分析之握手认证

介绍

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

与MySQL服务器的交互,可以分为四个阶段

建立TCP连接

握手认证阶段

命令执行阶段

断开连接

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

MySQL数据报文固定格式

MySQL的数据报文,由固定4byte的消息头和消息体组成。

其中消息头的前3个byte代表消息体的实际大小,第4个byte代表序号,用于保证消息顺序的正确。

e951ba535c9ce28987164fe965596d93.png

解析握手包

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

大小(单位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

}

复制代码

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

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

}

复制代码

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

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等等信息都可以直观的看到,接下来就是进行验证应答。

发送握手包响应数据

大小(单位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(

}

复制代码

确认认证结果,完成认证

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

2f3d2feb2e1d4cc11a2c168993a72e85.png

然后运行程序

b4c93fce140398cc9fabe86fb6b20068.png

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

总结

虽然在平常的工作中,不会自己编写连接驱动,但是秉着好奇的心,研究了一下mysql的连接过程,还是受益颇多~。

Thanks!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值