每次使用MySQL的时候,都是直接使用编写好的驱动,只关注业务部分。此次想探索一下链接的过程,所以有了此次总结。html
与MySQL服务器的交互,能够分为四个阶段mysql
这里主要探索握手认证的阶段,注意这里的握手认证,和TCP的三次握手不是同一个,是先创建了TCP链接,即已经完成了TCP三次握手,才进入到MySQL的握手认证。git
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的链接过程,仍是受益颇多~。
Thanks!