必须解析JSON后逐字段过滤string值,不可对原始字符串正则匹配;否则会误杀字段名、数字及转义字符,且无法处理嵌套结构。

直接结论:不要对原始 JSON 字符串做全文正则或简单字符串匹配,必须解析后逐字段过滤;否则会误杀字段名、数字、转义字符,且无法支持嵌套结构。
为什么不能用 strings.Contains 或正则扫 body
常见错误是读取 c.Request.Body 后直接用 strings.ReplaceAll 扫一遍——这会导致三类问题:
- JSON 字段名(如
"password"、"id")被当成敏感词替换,破坏结构 - 数字型值(如
"123")若在敏感词库中,会被错误替换为"***",导致解析失败 - 嵌套对象、数组、空值(
null)、转义字符("\u4f60")全部丢失语义,过滤结果不可靠
真正要过滤的,只有 JSON 中所有 string 类型的 value,且需保留原始类型和位置。
怎么安全地解析并过滤 JSON 字段
核心思路:用 json.RawMessage 递归遍历,只对 string 值调用前缀树(ahocorasick.Trie)匹配,其他类型跳过。
实操建议:
- 使用
json.Unmarshal将 body 解析为map[string]interface{}或自定义结构体(推荐前者,更灵活) - 写一个递归函数,遇到
string就传给trie.FindLongestPrefix判断是否命中,命中则替换为"***" - 避免反复
json.Marshal,用bytes.Buffer+json.NewEncoder直接写入新 body - 注意:如果原始 body 是数组(
[...]),顶层解析目标应为[]interface{},不能硬写成map
示例关键片段:
func filterJSONString(data []byte, trie *ahocorasick.Trie) []byte {
var raw interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return data // 解析失败,不处理
}
filtered := walkAndFilter(raw, trie)
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(filtered)
return buf.Bytes()
}
func walkAndFilter(v interface{}, trie *ahocorasick.Trie) interface{} {
switch x := v.(type) {
case string:
if trie.FindLongestPrefix(x) != nil {
return "***"
}
return x
case map[string]interface{}:
for k, val := range x {
x[k] = walkAndFilter(val, trie)
}
return x
case []interface{}:
for i, val := range x {
x[i] = walkAndFilter(val, trie)
}
return x
default:
return x
}
}
中间件里怎么正确重放 Request.Body
这是 Gin 中最易出错的一环:body 只能读一次,中间件修改后必须“还回去”,否则下游 handler 会读到空内容。
必须这么做:
- 用
io.ReadAll(c.Request.Body)一次性读完,得到[]byte - 调用过滤逻辑后,用
io.NopCloser(bytes.NewBuffer(filteredBody))包装成新ReadCloser - 赋值回
c.Request.Body—— 注意不是c.Request.Body = bytes.NewReader(...),因为bytes.Reader不实现io.Closer - 别忘了在
defer里关旧 body(虽然NopCloser的Close是空操作,但语义上要一致)
漏掉 io.NopCloser 会导致后续 c.ShouldBindJSON 报 invalid memory address or nil pointer dereference。
高频接口下怎么扛住 GC 压力
每次请求都 new bytes.Buffer、new map、反复 Unmarshal/Marshal,会显著抬高 GC 频率。
可落地的优化点:
- 用
sync.Pool复用*bytes.Buffer和map[string]interface{}实例 - 敏感词 Trie 构建一次,全局复用(
ahocorasick.NewTrie是线程安全的) - 如果业务允许,对特定路由(如
/api/v1/articles)启用过滤,而非全站中间件 - 考虑将过滤逻辑下沉到 GORM Hook 或 service 层,避开 HTTP body 解析开销(比如只过滤
Article.Title和Article.Content字段)
真正难的不是写过滤逻辑,而是让过滤不拖慢正常请求——90% 的性能损耗来自无意识的内存分配,而不是匹配本身。


















