奇怪的Go语言序列化问题
今天在编写 Go 语言的程序时,遇到了一个有关序列化的奇怪问题。这个问题花费了我一整个下午才最终解决,即使是解决之后,我依然对此十分困惑。
在 Go 语言的结构体中有一个结构体标签的语法,通过反引号来定义,接着就可以通过反射机制来取出结构体标签中的值。具体可以参考:
// 定义一个结构体 resume,包含两个字段:Name 和 sex
type resume struct {
// Name 字段,带有两个标签:info 和 doc
Name string `info:"name" doc:"我的名字"`
// sex 字段,带有一个标签:info
sex string `info:"sex"`
}
除此之外,结构体标签还可以用于 Json 格式的序列化编解码,比如在结构体标签中写上 Json 编码的字段名:
type Movie struct {
// 结构体字段的tag是用来指定json编码的时候的字段名
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"price"`
Actors []string `json:"actors"`
}
接着使用 "encoding/json"
包中的 Marshal
和 Unmarshal
就可以将结构体和 Json 编码互相转换。
// 序列化 编码过程 结构体 -> json字符串
movie := Movie{……}
jsonStr, err := json.Marshal(movie)
// 反序列化 解码过程 json字符串 -> 结构体
myMovie := Move{}
err = json.Unmarshal(jsonStr, &myMovie)
而今天我所遇到的问题,就是发生在这样序列化的互相转换中。
我定义了一个结构体 User
用于存放用户的一些信息,比如有用户名、密码等。
type User struct {
Username string `json:"username"`
Password string `json:"password"` // Bcrypt
Key string `json:"key"` // Bcrypt
GoogleSecret string `json:"google_secret"` // AES-256
}
其中的 GoogleSecret
存放的是 AES-256 加密过后的数据。
我将这个结构体通过 json.Marshal()
编码称 json 字符串并写入文件。后续还需要这些信息时,就去文件中读取 json 字符串并通过 json.Unmarshal()
转换会结构体变量。
这时问题就出现了,当我获取 GoogleSecret
使用 AES-256 解密时,时不时还出现 panic: crypto/cipher: input not full blocks 块大小不正确
的错误,但是我实现 AES 加密时已经使用了 PKCS7 填充。
这让我百思不得其解,我一开始以为是我的加密算法实现错误了,但是我的加密算法在使用之前已经进行了测试,并且能够成功加密字符串和文件。
后来我又去寻找是否是序列化的过程出错了,为此我还使用了 json.NewEncoder()
和 json.NewDecoder()
的方法重新实现了结构体和 json 的互相编解码,但是问题依然存在。
更让我困惑的是,我将 GoogleSecret
加密前、加密后和从文件中读取的数据打印出来逐一对比也没有发现问题。
直到我将 GoogleSecret
各个结点的哈希值打印出来,我才发现从文件中读取的数据是不正确的。
之前肉眼却看不出来,是因为加密后的数据都是字节流,直接转化成字符串都是乱码甚至显示不出来。而恰好文件读出的数据和原来的数据只有一点点差别。
就比如下面两个结构体数据看似一样,实际上它们的 hash 值完全不同:
&{2 $2a$12$k2Os6JvpSkLT8I50k334/uHBi9ghVcEMV3Sglh4hBp.Y1KxmUYUO. $2a$12$wx2mG4z/ciXTqUGtl6EJoeRrgjuQr99X9iv9e2ZcnunHaOVIQIYYm U��HԵ���q7��u'�W+
� �NE� rJ�$E�\���f����3�lP�/
c)��w�:��!��b>��� []}
&{2 $2a$12$k2Os6JvpSkLT8I50k334/uHBi9ghVcEMV3Sglh4hBp.Y1KxmUYUO. $2a$12$wx2mG4z/ciXTqUGtl6EJoeRrgjuQr99X9iv9e2ZcnunHaOVIQIYYm U��HԵ���q7��u'�W+
� �NE� rJ�$E�\���f����3�lP�/
c)��w�:��!��b>��� []}
所以,最终我定位到了是文件读出的 GoogleSecret
不正确,这与我将加密后的字节流直接转成字符串存放在变量中有关系。
查看 Go 语言标准库的文档,在 encoding/json 中提到Go 1.2及之后版本,编码器会强行将非法字节替换为unicode字符U+FFFD来使字符串合法。也就是说编码器会将 GoogleSecret
以 unicode 字符格式存放。比如这种形式:
"google_secret":"\r\ufffd\ufffd\u000b\ufffd={\r{\u0011\ufffdGO\ufffdO-\ufffd\u001e\u0005\ufffds!\ufffdM\ufffd$\ufffd\u003c\ufffd\u0007\n\ufffdu\ufffdjQ\u000e\ufffd \ufffdY\u0017\ufffd\ufffd\ufffdݷh\u0017\u003e^\u0002\ufffdu\ufffd\ufffd\"\ufffdk\ufffd\ufffd\ufffdy\u0006q\ufffd\ufffd\ufffd\ufffd\ufffd\u001fQO\ufffd\ufffd\ufffd\ufffd+\ufffdN\ufffdg\ufffd_8\ufffd\u001aO\ufffdL\u0018\ufffdl\ufffd\u0018\ufffd"
而之前直接将加密后的字节流转成字符串是 UTF-8 的格式的,有大部分数据是显示不出来的,比如这种形式:
"google_secret":"U��HԵ���q7��u'�W+
� �NE� rJ�$E�\���f����3�lP�/
c)��w�:��!��b>��� []}"
事实证明,如果使用这种乱码的形式进行 json 序列化,Go 语言编码器会将其转成 unicode 字符形式,而这一的转化会产生错误,导出数据信息丢失不能还原。
最后我将加密后的字节流转成十六进制的字符串存储,再通过 json 编解码就完全没有问题了,AES-256 加解密也完全不受影响。比如这种形式:
"google_secret":"dc4f8cbfe2b45bd2e19f98cd3f643b03f2f272a2a855e5b6a955ff35b36124f45b1f5c4efbb5b474a784dcf8e0acde31d5e8d5caf3c68f4208c56a4e24f907ba4e3c857b28e86ae3ff9344fca7b5ce62f6c30d0e1345fec673977876154fc047"
最后我依然很困惑,UTF-8 乱码形式到 Unicode 字符形式其中为什么会发生错误?还是说我猜测错了,其实并不是这里发生的错误?