每次使用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執行。雖然在日常的工做中,不會本身編寫鏈接驅動,可是秉着好奇的心,研究了一下mysql的鏈接過程,仍是受益頗多~。
Thanks!