奇怪的Go语言序列化问题

Keywords: #技术 #Golang #序列化

今天在编写 Go 语言的程序时,遇到了一个有关序列化的奇怪问题。这个问题花费了我一整个下午才最终解决,即使是解决之后,我依然对此十分困惑。

image.png

在 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" 包中的 MarshalUnmarshal 就可以将结构体和 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�����q7��u'�W+
 NE rJ�$E�\���f����3lP/
c)��w:��!��b>��� []}

&{2 $2a$12$k2Os6JvpSkLT8I50k334/uHBi9ghVcEMV3Sglh4hBp.Y1KxmUYUO. $2a$12$wx2mG4z/ciXTqUGtl6EJoeRrgjuQr99X9iv9e2ZcnunHaOVIQIYYm U�����q7��u'�W+
 NE rJ�$E�\���f����3lP/
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 字符形式其中为什么会发生错误?还是说我猜测错了,其实并不是这里发生的错误?