2026-06-21 22:53:37 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"sort"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"apigo.cc/go/cast"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// sanitizeOpts 日志脱敏配置
|
|
|
|
|
|
type sanitizeOpts struct {
|
|
|
|
|
|
maxSize int // 整体尺寸上限(内容字符估算)
|
|
|
|
|
|
fieldSize int // 单字符串截断长度
|
|
|
|
|
|
arrayNum int // 数组最多保留元素数
|
|
|
|
|
|
objectNum int // 对象最多保留 key 数
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// sanitizeLogData 递归脱敏,返回新建的对象,不影响原始数据
|
|
|
|
|
|
func sanitizeLogData(v any, opts sanitizeOpts) any {
|
|
|
|
|
|
budget := opts.maxSize
|
|
|
|
|
|
return sanitizeRecursive(v, opts, &budget)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func sanitizeRecursive(v any, opts sanitizeOpts, budget *int) any {
|
|
|
|
|
|
if v == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch val := v.(type) {
|
|
|
|
|
|
case string:
|
|
|
|
|
|
return sanitizeString(val, opts, budget)
|
|
|
|
|
|
case bool:
|
|
|
|
|
|
return sanitizeScalar(val, 5, budget)
|
|
|
|
|
|
case float64:
|
|
|
|
|
|
return sanitizeScalar(val, 8, budget)
|
|
|
|
|
|
case map[string]any:
|
|
|
|
|
|
return sanitizeMapContent(val, opts, budget)
|
|
|
|
|
|
case []any:
|
|
|
|
|
|
return sanitizeSliceContent(val, opts, budget)
|
|
|
|
|
|
default:
|
|
|
|
|
|
// int 等各种数值类型
|
|
|
|
|
|
return sanitizeScalar(val, 8, budget)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func sanitizeString(s string, opts sanitizeOpts, budget *int) string {
|
|
|
|
|
|
if len([]rune(s)) > opts.fieldSize {
|
|
|
|
|
|
s = string([]rune(s)[:opts.fieldSize])
|
|
|
|
|
|
}
|
|
|
|
|
|
if *budget < len(s) {
|
|
|
|
|
|
*budget = 0
|
|
|
|
|
|
return s // 即使超出预算也返回截断后的内容
|
|
|
|
|
|
}
|
|
|
|
|
|
*budget -= len(s)
|
|
|
|
|
|
return s
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func sanitizeScalar(v any, cost int, budget *int) any {
|
|
|
|
|
|
if *budget < cost {
|
|
|
|
|
|
*budget = 0
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
*budget -= cost
|
|
|
|
|
|
return v
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func sanitizeMapContent(m map[string]any, opts sanitizeOpts, budget *int) map[string]any {
|
|
|
|
|
|
// 空 map 占 2 预算
|
|
|
|
|
|
if *budget < 2 {
|
|
|
|
|
|
*budget = 0
|
|
|
|
|
|
return map[string]any{}
|
|
|
|
|
|
}
|
|
|
|
|
|
*budget -= 2
|
|
|
|
|
|
|
|
|
|
|
|
result := make(map[string]any)
|
|
|
|
|
|
// 排序 key 保证遍历顺序确定性
|
|
|
|
|
|
keys := make([]string, 0, len(m))
|
|
|
|
|
|
for k := range m {
|
|
|
|
|
|
keys = append(keys, k)
|
|
|
|
|
|
}
|
|
|
|
|
|
sort.Strings(keys)
|
|
|
|
|
|
|
|
|
|
|
|
count := 0
|
|
|
|
|
|
for _, k := range keys {
|
|
|
|
|
|
if count >= opts.objectNum || *budget <= 0 {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 扣 key 长度
|
|
|
|
|
|
keyCost := len(k)
|
|
|
|
|
|
if *budget < keyCost {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
budgetBefore := *budget
|
|
|
|
|
|
*budget -= keyCost
|
|
|
|
|
|
|
|
|
|
|
|
// 递归处理 value
|
|
|
|
|
|
processed := sanitizeRecursive(m[k], opts, budget)
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否因预算不足返回了 nil(仅当原值非 nil 时)
|
|
|
|
|
|
if processed == nil && m[k] != nil {
|
|
|
|
|
|
*budget = budgetBefore
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result[k] = processed
|
|
|
|
|
|
count++
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func sanitizeSliceContent(s []any, opts sanitizeOpts, budget *int) []any {
|
|
|
|
|
|
// 空 slice 占 2 预算
|
|
|
|
|
|
if *budget < 2 {
|
|
|
|
|
|
*budget = 0
|
|
|
|
|
|
return []any{}
|
|
|
|
|
|
}
|
|
|
|
|
|
*budget -= 2
|
|
|
|
|
|
|
|
|
|
|
|
result := make([]any, 0, min(len(s), opts.arrayNum))
|
|
|
|
|
|
count := 0
|
|
|
|
|
|
for _, v := range s {
|
|
|
|
|
|
if count >= opts.arrayNum || *budget <= 0 {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 值预算由递归处理
|
|
|
|
|
|
processed := sanitizeRecursive(v, opts, budget)
|
|
|
|
|
|
if processed == nil && v != nil {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result = append(result, processed)
|
|
|
|
|
|
count++
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 19:01:53 +08:00
|
|
|
|
// sanitizeLogHeaders 过滤请求/响应头,排除 noLogHeaders 中指定的字段。
|
|
|
|
|
|
// 对于 Cookie 头,不整体排除,而是从 Cookie 内容中剔除 key 命中排除列表的键值对。
|
2026-06-21 22:53:37 +08:00
|
|
|
|
func sanitizeLogHeaders(h http.Header, noLogHeaders string) map[string]string {
|
|
|
|
|
|
result := make(map[string]string)
|
2026-06-22 19:01:53 +08:00
|
|
|
|
excludes := splitAndTrim(noLogHeaders)
|
2026-06-21 22:53:37 +08:00
|
|
|
|
for k, v := range h {
|
2026-06-22 19:01:53 +08:00
|
|
|
|
if k == "Cookie" && len(excludes) > 0 {
|
|
|
|
|
|
if filtered := sanitizeCookieValue(v, excludes); filtered != "" {
|
|
|
|
|
|
result[k] = filtered
|
|
|
|
|
|
}
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-06-21 22:53:37 +08:00
|
|
|
|
skip := false
|
|
|
|
|
|
for _, ex := range excludes {
|
2026-06-22 19:01:53 +08:00
|
|
|
|
if strings.EqualFold(k, ex) {
|
2026-06-21 22:53:37 +08:00
|
|
|
|
skip = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !skip {
|
|
|
|
|
|
result[k] = strings.Join(v, ", ")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 19:01:53 +08:00
|
|
|
|
// sanitizeCookieValue 从 Cookie 原始值中剔除 key 命中排除列表的键值对。
|
|
|
|
|
|
// 输入为原始 Cookie 头值(可能多个 key=value 以 "; " 或 ";" 分隔)。
|
|
|
|
|
|
func sanitizeCookieValue(values []string, excludes []string) string {
|
|
|
|
|
|
raw := strings.Join(values, "; ")
|
|
|
|
|
|
pairs := strings.Split(raw, ";")
|
|
|
|
|
|
var kept []string
|
|
|
|
|
|
for _, pair := range pairs {
|
|
|
|
|
|
pair = strings.TrimSpace(pair)
|
|
|
|
|
|
if pair == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
key, _, _ := strings.Cut(pair, "=")
|
|
|
|
|
|
key = strings.TrimSpace(key)
|
|
|
|
|
|
excluded := false
|
|
|
|
|
|
for _, ex := range excludes {
|
|
|
|
|
|
if strings.EqualFold(key, ex) {
|
|
|
|
|
|
excluded = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !excluded {
|
|
|
|
|
|
kept = append(kept, pair)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.Join(kept, "; ")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func splitAndTrim(s string) []string {
|
|
|
|
|
|
if s == "" {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
parts := strings.Split(s, ",")
|
|
|
|
|
|
for i, p := range parts {
|
|
|
|
|
|
parts[i] = strings.TrimSpace(p)
|
|
|
|
|
|
}
|
|
|
|
|
|
return parts
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 22:53:37 +08:00
|
|
|
|
// sanitizeRespBody 对响应体进行脱敏:尝试 JSON 解析后走对象脱敏,失败则按字符串截断
|
|
|
|
|
|
func sanitizeRespBody(body []byte, cfg *ServiceConfig) any {
|
|
|
|
|
|
// 尝试解析为 JSON 对象
|
|
|
|
|
|
var parsed any
|
|
|
|
|
|
if err := cast.UnmarshalJSON(body, &parsed); err == nil && parsed != nil {
|
|
|
|
|
|
// 排除敏感字段
|
2026-06-22 19:01:53 +08:00
|
|
|
|
if cfg.NoLogResponseFields != "" {
|
|
|
|
|
|
parsed = stripFields(parsed, cfg.NoLogResponseFields)
|
2026-06-21 22:53:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
return sanitizeLogData(parsed, sanitizeOpts{
|
|
|
|
|
|
maxSize: 200,
|
|
|
|
|
|
fieldSize: cfg.LogOutputFieldSize,
|
|
|
|
|
|
arrayNum: cfg.LogOutputArrayNum,
|
|
|
|
|
|
objectNum: cfg.LogOutputObjectNum,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 非 JSON 内容,按字符串截断
|
|
|
|
|
|
if len(body) > cfg.LogOutputMaxSize {
|
|
|
|
|
|
return string(body[:cfg.LogOutputMaxSize]) + "..."
|
|
|
|
|
|
}
|
|
|
|
|
|
return string(body)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// stripFields 从对象中删除指定字段(仅处理顶层 map)
|
|
|
|
|
|
func stripFields(v any, fields string) any {
|
|
|
|
|
|
m, ok := v.(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return v
|
|
|
|
|
|
}
|
|
|
|
|
|
excludes := strings.Split(fields, ",")
|
|
|
|
|
|
for _, f := range excludes {
|
|
|
|
|
|
f = strings.TrimSpace(f)
|
|
|
|
|
|
if f != "" {
|
|
|
|
|
|
delete(m, f)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return m
|
|
|
|
|
|
}
|