阿小信大人的头像
Where there is a Python, there is a way. 阿小信大人

Golang JSON序列化时HTML字符被转移问题分析2021-11-21 18:13

场景

在API实现中返回一个json结果,其中有一个字段为URL链接,客户端拿到该链接后做请求,URL链接中存在多个使用 & 连接的 query string 参数。服务端实现时,通过构造结构体后返回对应的json数据。

但是请求接口时发现 URL 链接中的 & 符号被 Golang 自动转义为 \u0026,导致客户端无法解析URL中的参数。

一段代码模拟该场景:

package main

import (
    "encoding/json"
    "fmt"
)

type Data struct {
    Link string
}

func main() {

    link := `https://mbti.axiaoxin.com/?lang=ko&from=article`
    data := Data{
        Link: link,
    }
    b, _ := json.Marshal(data)
    fmt.Println("data json:", string(b))
    fmt.Println("---")
}

代码执行结果输出:

data json: {"Link":"https://mbti.axiaoxin.com/?lang=ko\u0026from=article"}
---

其中的 & 变成了 \u0026

分析

golang的json默认会对特殊的html字符进行转义处理。

json.Marshal 的实现:

func Marshal(v interface{}) ([]byte, error) {
    e := newEncodeState()

    err := e.marshal(v, encOpts{escapeHTML: true})
    if err != nil {
        return nil, err
    }
    buf := append([]byte(nil), e.Bytes()...)

    encodeStatePool.Put(e)

    return buf, nil
}

其中 e.marshal 通过 escapeHTML: true 指定了 encode 时需要做 HTMLEscape 处理。

其中通过判断需要序列化对象的类型,来使用对应的 encoderFunc 来做序列化。

这里我们是结构体,因此会调用 structEncoder 的 encode 方法来处理:

image

可以看到 encode 方法除了会对结构体字段的值做 escape 处理外,对结构体的 json tag 名也会做 escape 处理,实验一下:

package main

import (
    "encoding/json"
    "fmt"
)

type Data struct {
    Link string `json:"Li&nk"`
}

func main() {

    link := `https://mbti.axiaoxin.com/?lang=ko&from=article`
    data := Data{
        Link: link,
    }
    b, _ := json.Marshal(data)
    fmt.Println("data json:", string(b))
    fmt.Println("---")
}

运行输出:

data json: {"Li\u0026nk":"https://mbti.axiaoxin.com/?lang=ko\u0026from=article"}
---

对于结构体的每个字段,structEncoder 都会将其转换为 field 对象:

// A field represents a single field found in a struct.
type field struct {
    name      string
    nameBytes []byte                 // []byte(name)
    equalFold func(s, t []byte) bool // bytes.EqualFold or equivalent

    nameNonEsc  string // `"` + name + `":`
    nameEscHTML string // `"` + HTMLEscape(name) + `":`

    tag       bool
    index     []int
    typ       reflect.Type
    omitEmpty bool
    quoted    bool

    encoder encoderFunc
}

结构体字段值通过自身类型的 encoderFunc 进行处理,这里我们是 string 类型,因此将调用 stringEncoder 对值进行处理:

image

stringEncoder 中 调用 string 方法对字符串中 RuneSelf 以下的字符(ASCII)进行 escape 处理,其中就对 & 进行了替换:

image

htmlSafeSet是一个 html 特殊符号是否安全的 map,& 符号返回 false, 因此进行了hex的运算被替换。

解决

解决方法 golang 在源码中已说明白:

// String values encode as JSON strings coerced to valid UTF-8,
// replacing invalid bytes with the Unicode replacement rune.
// So that the JSON will be safe to embed inside HTML <script> tags,
// the string is encoded using HTMLEscape,
// which replaces "<", ">", "&", U+2028, and U+2029 are escaped
// to "\u003c","\u003e", "\u0026", "\u2028", and "\u2029".
// This replacement can be disabled when using an Encoder,
// by calling SetEscapeHTML(false).

可以通过 json.NewEncoder 创建一个新的 encoder,再对该 encoder 设置 SetEscapeHTML(false) 封装除自定义的 encoder。

// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
    return &Encoder{w: w, escapeHTML: true}
}

可见NewEncoder默认也是设置escapeHTML: true,但是它提供了SetEscapeHTML方法来修改这个设置,然后调用他的 Encode 方法进行序列化。

这里需要注意 NewEncoder 的 Encode 方法会在json序列化结果后添加一个 \n:

image

代码实现:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
)

type Data struct {
    Link string `json:"Li&nk"`
}

func Encoding(t interface{}) ([]byte, error) {
    buffer := &bytes.Buffer{}

    encoder := json.NewEncoder(buffer)
    encoder.SetEscapeHTML(false)
    err := encoder.Encode(t)
    return buffer.Bytes(), err
}

func main() {

    link := `https://mbti.axiaoxin.com/?lang=ko&from=article`
    data := Data{
        Link: link,
    }
    b, _ := json.Marshal(data)
    fmt.Println("data json:", string(b))
    fmt.Println("---")

    b, _ = Encoding(data)
    fmt.Println("data json:", string(b))
    fmt.Println("---")

}

执行结果:

data json: {"Li\u0026nk":"https://mbti.axiaoxin.com/?lang=ko\u0026from=article"}
---
data json: {"Li&nk":"https://mbti.axiaoxin.com/?lang=ko&from=article"}

---

注意第二个打印多了一个空行。

在 web 框架中,比如 echo,其 New 方法实现:

image

其中的JSONSerializer没有设置 escape 为false,因此使用 echo 做开发,返回的json都会出现这种问题:

image

复制JSONSerializer的代码,改为自己的 MyJSONSerializer ,在Serialize方法中添加 enc.SetEscapeHTML(false) 来实现不 escape,在调用 echo 的 New 方法后,再把他的JSONSerializer用我们新的 JSONSerializer 覆盖即可。

e := echo.New()
e.JSONSerializer = &MyJSONSerializer{}

如果您觉得从我的分享中得到了帮助,并且希望我的博客持续发展下去,请点击支付宝捐赠,谢谢!

若非特别声明,文章均为阿小信的个人笔记,转载请注明出处。文章如有侵权内容,请联系我,我会及时删除。

#Golang#   阅读[128] 评论[0]

下一篇:已经是最后一篇

你可能也感兴趣的文章推荐

本文最近访客

网友77.*.*.6[火星]2021-12-05 10:31
网友157.*.*.140[Redmond]2021-12-05 10:30
网友216.*.*.226[Seattle]2021-12-05 10:22
网友77.*.*.5[火星]2021-12-05 09:15

发表评论