Golang JSON 序列化时 HTML 特殊字符转义问题详解及解决方案

文章目录

在实际开发中,Golang 常用于构建高性能 API 接口。在返回 JSON 结果时,特别是涉及包含特殊字符的 URL 字段,开发者可能会遇到字符被自动转义的情况。常见的是 & 符号被转义为 \u0026,导致客户端无法正常解析该链接的参数,进而影响请求的正确性。

复现问题

在 API 实现中返回一个 JSON 结果,其中有一个字段为 URL 链接,客户端拿到该链接后做请求,URL 链接中存在多个使用 & 连接的 querystring 参数。

服务端实现时,通过构造结构体后返回对应的 JSON 序列化结果。

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

一段代码模拟该场景:

package main

import (
    "encoding/json"
    "fmt"
)

type Data struct {
    Link string
}

func main() {

    link := `https://blog.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://blog.axiaoxin.com/?lang=ko\u0026from=article"}
---

可以看到,URL 中的 & 符号被替换为了 \u0026,这正是由于 Go 在 JSON 序列化过程中对 HTML 特殊字符进行转义所致。

转义原因分析:为什么 & 符号会替换为 \u0026

Golang 在使用 json.Marshal 序列化时,默认会对一些 HTML 特殊字符(如 &, <, > 等)进行转义。这种行为是为了确保 JSON 能够安全地嵌入 HTML 页面,防止潜在的 XSS 攻击。在 json.Marshal 的源码中,可以看到默认开启了 escapeHTML: true 参数:

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
}

escapeHTML 参数的设置直接影响到字符的处理方式,比如 & 会被转义为 \u0026,影响 URL 的传递。

其中 e.marshal 通过 escapeHTML: true 指定了 encode 时需要做 HTMLEscape 处理,即对 HTML 特殊字符进行转义。

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

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

Golang JSON Marshal 源码分析

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

package main

import (
    "encoding/json"
    "fmt"
)

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

func main() {

    link := `https://blog.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://blog.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 对值进行处理:

Golang JSON Marshal 源码分析

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

Golang JSON Marshal 源码分析

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

解决方案

针对这一问题,最简单的方法是使用 json.NewEncoder,并通过 SetEscapeHTML(false) 来取消默认的 HTML 特殊字符转义。

解决方法 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:

Golang JSON Marshal 源码分析

代码实现:

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://blog.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("---")
}

运行后,结果将正确保留 URL 中的 & 符号:

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

---

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

通过这种方法,能够避免 & 被错误转义成 \u0026,确保客户端可以正确解析 URL 参数。

在 Golang Web 框架 echo 中的应用:自定义 JSON 序列化方法

很多开发者在使用 Go 进行 Web 开发时,常常使用像 Echo 这样的框架。在 Echo 中,JSON 序列化是默认启用了 escapeHTML: true 的,因此开发者需要手动修改。比如 echo,其 New 方法实现:

echo 框架中自定义 JSON 序列化方法

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

echo 框架中自定义 JSON 序列化方法

可以通过自定义 JSON 序列化方法来解决该问题。在 Echo 框架中实现自定义 JSON 序列化器,重写其中的 Serialize 方法,并添加 SetEscapeHTML(false),可以有效避免 HTML 特殊字符的转义问题。

复制 JSONSerializer 的代码,改为自己的 MyJSONSerializer ,在 Serialize 方法中添加 enc.SetEscapeHTML(false) 来实现不 escape。

在调用 echo 的 New 方法后,再把他的 JSONSerializer 用我们新的 JSONSerializer 覆盖即可。

代码示例:

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

FAQ

Q: 为什么 JSON 序列化时会自动转义 & A: Go 默认会对一些 HTML 特殊字符进行转义,以防止潜在的 XSS 攻击,& 被转义为 \u0026 是这种行为的表现。

Q: 如何关闭 Golang 的 JSON 序列化字符转义? A: 可以使用 json.NewEncoder 并调用 SetEscapeHTML(false) 方法来取消默认的转义行为。

Q: Echo 框架中如何自定义 JSON 序列化方式? A: 在 Echo 框架中,可以通过自定义 JSONSerializer 并覆盖 SetEscapeHTML(false) 来解决。

相关阅读推荐:Golang JSON 操作完全指南:从基础到进阶,看这一篇就够了!


也可以看看