package httplog import ( "encoding/json" "mime" "strings" "github.com/cloudwego/hertz/pkg/app" ) var sensitiveKeys = []string{"password", "secret", "token", "key", "passwd"} const redactedValue = "[REDACTED]" // desensitization:只对第一层 key 做脱敏,不递归 func desensitization(data map[string]any) ([]byte, error) { out := make(map[string]any, len(data)) for k, v := range data { if isSensitiveKey(k) { out[k] = redactedValue } else { out[k] = v } } return json.Marshal(out) } // 大小写不敏感、包含式匹配 func isSensitiveKey(key string) bool { k := strings.ToLower(key) for _, s := range sensitiveKeys { if strings.Contains(k, s) { return true } } return false } // FormBodyToJSONWithFilePlaceholder : // - 普通表单:完整转 JSON // - multipart 文件字段:不取任何文件信息,只输出 "[file]" 或 ["[file]", ...] func FormBodyToJSONWithFilePlaceholder(c *app.RequestContext) (jsonBytes []byte, ok bool, err error) { ct := string(c.Request.Header.ContentType()) mediaType, _, _ := mime.ParseMediaType(ct) mediaType = strings.ToLower(mediaType) out := make(map[string]any) switch mediaType { case "application/x-www-form-urlencoded": args := c.PostArgs() args.VisitAll(func(k, v []byte) { key := string(k) // 同名 key 多次出现时,转成数组 if old, exists := out[key]; exists { switch x := old.(type) { case string: out[key] = []string{x, string(v)} case []string: out[key] = append(x, string(v)) default: out[key] = string(v) } } else { out[key] = string(v) } }) b, e := desensitization(out) return b, true, e case "multipart/form-data": form, e := c.MultipartForm() if e != nil { return nil, false, e } // 普通字段 for k, vv := range form.Value { if len(vv) == 1 { out[k] = vv[0] } else { out[k] = vv } } // 文件字段:只做占位,不读取任何文件信息 for k, files := range form.File { if len(files) <= 1 { out[k] = "[file]" } else { arr := make([]string, 0, len(files)) for range files { arr = append(arr, "[file]") } out[k] = arr } } b, e2 := desensitization(out) return b, true, e2 default: // 不是表单就不处理 return nil, false, nil } } // ResponseBodySnippet :获取响应内容 func ResponseBodySnippet(c *app.RequestContext, maxBytes int) (body string, ok bool, truncated bool) { b := c.Response.Body() if len(b) == 0 { return "", false, false } // 只采集“看起来像文本/JSON”的响应,其他(图片/zip等)直接跳过 ct := strings.ToLower(string(c.Response.Header.ContentType())) if ct != "" && !(strings.HasPrefix(ct, "text/") || strings.Contains(ct, "application/json") || strings.Contains(ct, "application/xml") || strings.Contains(ct, "application/javascript")) { return "", false, false } if maxBytes > 0 && len(b) > maxBytes { b = b[:maxBytes] truncated = true } body = string(b) return body, true, truncated }