[ 生活需要仪式感 ]

0%

Goland 打平无结构化的 Json 文本进行脱敏打码

1 核心原理

如果是正常 json,goland 可以默认解析为 4 种结构之一,只要进行类型断言并进行路径计算,即可在指定的位置进行脱敏。

2 Demo

举例,以下是一段来自 JSON官方范例 的代码片段。
我加入了 2 个敏感数据字段 widget.image.telephone, widget.text.phoneNumber
若要对这 2 个字段进行掩码脱敏,要如何操作?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{"widget": {
"debug": "on",
"window": {
"title": "Sample Mask Demo",
"name": "main_window",
"width": 500,
"height": 500
},
"image": {
"src": "Images/Sun.png",
"name": "sun1",
"hOffset": 250,
"vOffset": 250,
"telephone": "13800138888",
"alignment": "center"
},
"text": {
"data": "Click Here",
"size": 36,
"phoneNumber": "13800138000",
"style": "bold",
"name": "text1",
"hOffset": 250,
"vOffset": 100,
"alignment": "center",
"onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"
}
}}

3 解决方案

3.1 过往方案

常见方案,也是我一开始想到的:给这个 json 定义一个结构体(struct),并收到报文时进行反解析,对其中固定的字段进行掩码操作。

但是这个有几个弊端:

  1. 要依赖反解析数据,如果来的是一段 []byte, 或者 string,如何操作?
  2. 非常依赖稳定的 json 结构,如果来的报文格式多样,没有稳定的结构格式,如何解决?

3.2 新思路

针对问题 1,我是通过一种通用解析的方案来解决:

json 反解析到结构,如果不指定具体的结构类型,那么 json 会被反解析为以下几种类型之一:

  • bool
  • string
  • map[string]interface{}
  • []interface{}

那么我索性直接使用通用模式,用 switch data := inputData.(type) 进行类型断言,每次根据断言来判断对应类型操作,同时以递归为工具,一直解析到最底层。


针对问题 2,我是通过打平的方案来解决:

既然报文格式多样,那我就不再定义特定的结构格式,而是通过打平 json,比如上面的例子,我打平为:

  • widget.debug = on
  • widget.image.telephone = 13800138888

这样的格式进行处理,那么在做脱敏的时候,只需要传入 json内容需要脱敏的位置 即可进行处理。

3.3 代码演示

[GoPlayGroud 在线运行]

源码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package main

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

// 演示 Json
const demoJson = `{"widget":{"debug":"on","window":{"title":"Sample Mask Demo","name":"main_window","width":500,"height":500},"image":{"src":"Images/Sun.png","name":"sun1","hOffset":250,"vOffset":250,"telephone":"13800138888","alignment":"center"},"text":{"data":"Click Here","size":36,"phoneNumber":"13800138000","style":"bold","name":"text1","hOffset":250,"vOffset":100,"alignment":"center","onMouseUp":"sun1.opacity = (sun1.opacity / 100) * 90;"}}}`

// 打平风格符号
const JStringSep = "."

// 最大递归深度
const JStringMaxDepth = 10

func main() {
// 赋值数据
var data map[string]interface{}
err := json.Unmarshal([]byte(demoJson), &data)
if err != nil {
panic(err)
}
// 脱敏处理
_, _ = dfs(getDfsPath(), "", data)
// 输出处理
res, err := json.Marshal(data)
if err != nil {
panic(err)
}
fmt.Println(string(res))
return
}

// getDfsPath 获取递归参数
func getDfsPath() (result map[string]string) {
result = make(map[string]string, 0)
result = map[string]string{
".widget.image.telephone": "telephone",
".widget.text.phoneNumber": "telephone",
}
return
}

// dfs 递归
func dfs(searchPath map[string]string, inputPath string, inputData interface{}) (path string, result interface{}) {
// 防止无限递归
if split := strings.Split(inputPath, JStringSep); len(split) > JStringMaxDepth {
fmt.Printf("深度大于[%d],异常", JStringMaxDepth)
path = inputPath
result = inputData
return
}

switch data := inputData.(type) {
case bool:
return
case string:
path = inputPath
result = data
// 具体脱敏处理
if handler, ok := searchPath[path]; ok {
switch handler {
//case "name" ....
case "telephone":
result = data[:3] + "****" + data[7:]
}
return
}
return
case map[string]interface{}:
for key, item := range data {
path, result = dfs(searchPath, inputPath+JStringSep+key, item)
if _, ok := searchPath[path]; ok {
data[key] = result
}
}
return
case []interface{}:
for key, item := range data {
path, result = dfs(searchPath, inputPath, item)
if _, ok := searchPath[path]; ok {
data[key] = result

}
}
return
}
return
}