| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- // Package pii 提供字段级 PII 脱敏(规则拷贝自旧 MCP 的 helper/pii_*.go,两者独立维护)。
- package pii
- import (
- "bytes"
- "encoding/json"
- "fmt"
- "strconv"
- )
- // MaskJSONBytes 对已序列化的 JSON 字节流做流式脱敏,返回脱敏后的 JSON 字节。
- //
- // 实现要点(优化 B):
- // - 使用 json.Decoder 的 token 流式解析,不构建完整的 interface{} 对象树,
- // 内存占用为常数级(栈深度 = JSON 嵌套深度)。
- // - 单次遍历:读到 object 的 key 命中敏感规则时,对其对应的 value 做脱敏;
- // 其余 token 原样透传。
- // - 若敏感 key 的 value 是对象或数组,则整个子树被完全脱敏(fail-safe)。
- // - 解析失败时返回 error(fail-closed,由上层拒绝输出,不泄露原始数据)。
- func MaskJSONBytes(in []byte) ([]byte, error) {
- dec := json.NewDecoder(bytes.NewReader(in))
- dec.UseNumber()
- var out bytes.Buffer
- w := &jsonWriter{buf: &out}
- if err := maskValue(dec, w, nil); err != nil {
- return nil, fmt.Errorf("PII 脱敏失败: %w", err)
- }
- // 确认没有多余内容(例如尾部垃圾),保证输入是单个合法 JSON 值。
- if dec.More() {
- return nil, fmt.Errorf("PII 脱敏失败: JSON 存在多余的尾部内容")
- }
- return out.Bytes(), nil
- }
- // maskValue 读取下一个 JSON 值并写入 w。
- // forceMask 非 nil 时,表示当前值处于某个敏感 key 之下,整个值(含子树)需完全脱敏。
- func maskValue(dec *json.Decoder, w *jsonWriter, forceMask *MaskKind) error {
- tok, err := dec.Token()
- if err != nil {
- return err
- }
- switch t := tok.(type) {
- case json.Delim:
- switch t {
- case '{':
- return maskObject(dec, w, forceMask)
- case '[':
- return maskArray(dec, w, forceMask)
- default:
- return fmt.Errorf("非预期的分隔符: %v", t)
- }
- default:
- // 标量值(string / json.Number / bool / nil)
- return writeScalar(w, tok, forceMask)
- }
- }
- // maskObject 处理一个 JSON 对象。forceMask 非 nil 时整个对象子树被脱敏。
- func maskObject(dec *json.Decoder, w *jsonWriter, forceMask *MaskKind) error {
- w.openObject()
- for dec.More() {
- // 读 key
- keyTok, err := dec.Token()
- if err != nil {
- return err
- }
- key, ok := keyTok.(string)
- if !ok {
- return fmt.Errorf("对象 key 非字符串: %T", keyTok)
- }
- w.writeKey(key)
- // 决定该字段 value 的脱敏方式:
- // - 若已处于强制脱敏子树中,继续沿用父级 forceMask。
- // - 否则查规则表。
- var childForce *MaskKind
- if forceMask != nil {
- childForce = forceMask
- } else if kind, hit := maskKindFor(key); hit {
- k := kind
- childForce = &k
- }
- if err := maskValue(dec, w, childForce); err != nil {
- return err
- }
- }
- // 消费结束的 '}'
- if _, err := dec.Token(); err != nil {
- return err
- }
- w.closeObject()
- return nil
- }
- // maskArray 处理一个 JSON 数组。forceMask 非 nil 时数组内每个元素都被脱敏。
- func maskArray(dec *json.Decoder, w *jsonWriter, forceMask *MaskKind) error {
- w.openArray()
- for dec.More() {
- if err := maskValue(dec, w, forceMask); err != nil {
- return err
- }
- }
- // 消费结束的 ']'
- if _, err := dec.Token(); err != nil {
- return err
- }
- w.closeArray()
- return nil
- }
- // writeScalar 写入一个标量值,必要时脱敏。
- func writeScalar(w *jsonWriter, tok json.Token, forceMask *MaskKind) error {
- if forceMask != nil {
- // 处于敏感字段之下:
- // - 字符串按具体 MaskKind 处理
- // - 其余标量(数字/bool,例如经纬度以 number 存储)统一替换为掩码字符串
- if s, ok := tok.(string); ok {
- return w.writeValueRaw(quote(applyMaskString(*forceMask, s)))
- }
- if tok == nil {
- // null 保持 null(无信息泄露)
- return w.writeValueRaw("null")
- }
- return w.writeValueRaw(quote(maskFull()))
- }
- // 非敏感字段:原样写出
- switch v := tok.(type) {
- case string:
- return w.writeValueRaw(quote(v))
- case json.Number:
- return w.writeValueRaw(v.String())
- case bool:
- return w.writeValueRaw(strconv.FormatBool(v))
- case nil:
- return w.writeValueRaw("null")
- default:
- return fmt.Errorf("非预期的标量类型: %T", tok)
- }
- }
- // quote 将字符串按 JSON 规则转义并加引号。
- func quote(s string) string {
- b, _ := json.Marshal(s)
- return string(b)
- }
- // jsonWriter 负责按结构写出 JSON,管理逗号与 key/value 状态。
- type jsonWriter struct {
- buf *bytes.Buffer
- stack []*frame
- }
- type frame struct {
- isObject bool
- needComma bool // 是否需要在下一个元素/键前加逗号
- expectKey bool // 对象中:下一个 writeKey 之后期待 value
- pendingKey bool // 已写入 key,等待 value(value 前不加逗号)
- }
- func (w *jsonWriter) top() *frame {
- if len(w.stack) == 0 {
- return nil
- }
- return w.stack[len(w.stack)-1]
- }
- // beforeValue 在写入一个值(标量或容器起始)之前处理逗号。
- func (w *jsonWriter) beforeValue() {
- f := w.top()
- if f == nil {
- return
- }
- if f.isObject {
- // 对象中值紧跟 key,key 已处理逗号,这里不加
- if f.pendingKey {
- f.pendingKey = false
- return
- }
- }
- if f.needComma {
- w.buf.WriteByte(',')
- }
- f.needComma = true
- }
- func (w *jsonWriter) openObject() {
- w.beforeValue()
- w.buf.WriteByte('{')
- w.stack = append(w.stack, &frame{isObject: true})
- }
- func (w *jsonWriter) closeObject() {
- w.buf.WriteByte('}')
- w.stack = w.stack[:len(w.stack)-1]
- }
- func (w *jsonWriter) openArray() {
- w.beforeValue()
- w.buf.WriteByte('[')
- w.stack = append(w.stack, &frame{isObject: false})
- }
- func (w *jsonWriter) closeArray() {
- w.buf.WriteByte(']')
- w.stack = w.stack[:len(w.stack)-1]
- }
- func (w *jsonWriter) writeKey(key string) {
- f := w.top()
- if f != nil && f.needComma {
- w.buf.WriteByte(',')
- }
- w.buf.WriteString(quote(key))
- w.buf.WriteByte(':')
- if f != nil {
- f.needComma = true
- f.pendingKey = true
- }
- }
- func (w *jsonWriter) writeValueRaw(raw string) error {
- w.beforeValue()
- _, err := w.buf.WriteString(raw)
- return err
- }
- // Marshal 将任意数据序列化为紧凑 JSON 并完成字段级脱敏。
- // 脱敏失败时返回错误(fail-closed),调用方不得输出原始数据。
- func Marshal(v any) ([]byte, error) {
- raw, err := json.Marshal(v)
- if err != nil {
- return nil, fmt.Errorf("JSON 序列化失败: %w", err)
- }
- return MaskJSONBytes(raw)
- }
- // MaskField 按字段名对单个字符串值脱敏(表格/CSV 单元格使用);字段不在规则表中时原样返回。
- func MaskField(key, value string) string {
- kind, ok := maskKindFor(key)
- if !ok || value == "" {
- return value
- }
- return applyMaskString(kind, value)
- }
|