redact.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. package logredact
  2. import (
  3. "encoding/json"
  4. "regexp"
  5. "sort"
  6. "strings"
  7. "sync"
  8. )
  9. // maxRedactDepth 限制递归深度以防止栈溢出
  10. const maxRedactDepth = 32
  11. var defaultSensitiveKeys = map[string]struct{}{
  12. "authorization_code": {},
  13. "code": {},
  14. "code_verifier": {},
  15. "access_token": {},
  16. "refresh_token": {},
  17. "id_token": {},
  18. "client_secret": {},
  19. "password": {},
  20. }
  21. var defaultSensitiveKeyList = []string{
  22. "authorization_code",
  23. "code",
  24. "code_verifier",
  25. "access_token",
  26. "refresh_token",
  27. "id_token",
  28. "client_secret",
  29. "password",
  30. }
  31. type textRedactPatterns struct {
  32. reJSONLike *regexp.Regexp
  33. reQueryLike *regexp.Regexp
  34. rePlain *regexp.Regexp
  35. }
  36. var (
  37. reGOCSPX = regexp.MustCompile(`GOCSPX-[0-9A-Za-z_-]{24,}`)
  38. reAIza = regexp.MustCompile(`AIza[0-9A-Za-z_-]{35}`)
  39. defaultTextRedactPatterns = compileTextRedactPatterns(nil)
  40. extraTextPatternCache sync.Map // map[string]*textRedactPatterns
  41. )
  42. func RedactMap(input map[string]any, extraKeys ...string) map[string]any {
  43. if input == nil {
  44. return map[string]any{}
  45. }
  46. keys := buildKeySet(extraKeys)
  47. redacted, ok := redactValueWithDepth(input, keys, 0).(map[string]any)
  48. if !ok {
  49. return map[string]any{}
  50. }
  51. return redacted
  52. }
  53. func RedactJSON(raw []byte, extraKeys ...string) string {
  54. if len(raw) == 0 {
  55. return ""
  56. }
  57. var value any
  58. if err := json.Unmarshal(raw, &value); err != nil {
  59. return "<non-json payload redacted>"
  60. }
  61. keys := buildKeySet(extraKeys)
  62. redacted := redactValueWithDepth(value, keys, 0)
  63. encoded, err := json.Marshal(redacted)
  64. if err != nil {
  65. return "<redacted>"
  66. }
  67. return string(encoded)
  68. }
  69. // RedactText 对非结构化文本做轻量脱敏。
  70. //
  71. // 规则:
  72. // - 如果文本本身是 JSON,则按 RedactJSON 处理。
  73. // - 否则尝试对常见 key=value / key:"value" 片段做脱敏。
  74. //
  75. // 注意:该函数用于日志/错误信息兜底,不保证覆盖所有格式。
  76. func RedactText(input string, extraKeys ...string) string {
  77. input = strings.TrimSpace(input)
  78. if input == "" {
  79. return ""
  80. }
  81. raw := []byte(input)
  82. if json.Valid(raw) {
  83. return RedactJSON(raw, extraKeys...)
  84. }
  85. patterns := getTextRedactPatterns(extraKeys)
  86. out := input
  87. out = reGOCSPX.ReplaceAllString(out, "GOCSPX-***")
  88. out = reAIza.ReplaceAllString(out, "AIza***")
  89. out = patterns.reJSONLike.ReplaceAllString(out, `$1***$3`)
  90. out = patterns.reQueryLike.ReplaceAllString(out, `$1=***`)
  91. out = patterns.rePlain.ReplaceAllString(out, `$1$2***`)
  92. return out
  93. }
  94. func compileTextRedactPatterns(extraKeys []string) *textRedactPatterns {
  95. keyAlt := buildKeyAlternation(extraKeys)
  96. return &textRedactPatterns{
  97. // JSON-like: "access_token":"..."
  98. reJSONLike: regexp.MustCompile(`(?i)("(?:` + keyAlt + `)"\s*:\s*")([^"]*)(")`),
  99. // Query-like: access_token=...
  100. reQueryLike: regexp.MustCompile(`(?i)\b((?:` + keyAlt + `))=([^&\s]+)`),
  101. // Plain: access_token: ... / access_token = ...
  102. rePlain: regexp.MustCompile(`(?i)\b((?:` + keyAlt + `))\b(\s*[:=]\s*)([^,\s]+)`),
  103. }
  104. }
  105. func getTextRedactPatterns(extraKeys []string) *textRedactPatterns {
  106. normalizedExtraKeys := normalizeAndSortExtraKeys(extraKeys)
  107. if len(normalizedExtraKeys) == 0 {
  108. return defaultTextRedactPatterns
  109. }
  110. cacheKey := strings.Join(normalizedExtraKeys, ",")
  111. if cached, ok := extraTextPatternCache.Load(cacheKey); ok {
  112. if patterns, ok := cached.(*textRedactPatterns); ok {
  113. return patterns
  114. }
  115. }
  116. compiled := compileTextRedactPatterns(normalizedExtraKeys)
  117. actual, _ := extraTextPatternCache.LoadOrStore(cacheKey, compiled)
  118. if patterns, ok := actual.(*textRedactPatterns); ok {
  119. return patterns
  120. }
  121. return compiled
  122. }
  123. func normalizeAndSortExtraKeys(extraKeys []string) []string {
  124. if len(extraKeys) == 0 {
  125. return nil
  126. }
  127. seen := make(map[string]struct{}, len(extraKeys))
  128. keys := make([]string, 0, len(extraKeys))
  129. for _, key := range extraKeys {
  130. normalized := normalizeKey(key)
  131. if normalized == "" {
  132. continue
  133. }
  134. if _, ok := seen[normalized]; ok {
  135. continue
  136. }
  137. seen[normalized] = struct{}{}
  138. keys = append(keys, normalized)
  139. }
  140. sort.Strings(keys)
  141. return keys
  142. }
  143. func buildKeyAlternation(extraKeys []string) string {
  144. seen := make(map[string]struct{}, len(defaultSensitiveKeyList)+len(extraKeys))
  145. keys := make([]string, 0, len(defaultSensitiveKeyList)+len(extraKeys))
  146. for _, k := range defaultSensitiveKeyList {
  147. seen[k] = struct{}{}
  148. keys = append(keys, regexp.QuoteMeta(k))
  149. }
  150. for _, k := range extraKeys {
  151. n := normalizeKey(k)
  152. if n == "" {
  153. continue
  154. }
  155. if _, ok := seen[n]; ok {
  156. continue
  157. }
  158. seen[n] = struct{}{}
  159. keys = append(keys, regexp.QuoteMeta(n))
  160. }
  161. return strings.Join(keys, "|")
  162. }
  163. func buildKeySet(extraKeys []string) map[string]struct{} {
  164. keys := make(map[string]struct{}, len(defaultSensitiveKeys)+len(extraKeys))
  165. for k := range defaultSensitiveKeys {
  166. keys[k] = struct{}{}
  167. }
  168. for _, key := range extraKeys {
  169. normalized := normalizeKey(key)
  170. if normalized == "" {
  171. continue
  172. }
  173. keys[normalized] = struct{}{}
  174. }
  175. return keys
  176. }
  177. func redactValueWithDepth(value any, keys map[string]struct{}, depth int) any {
  178. if depth > maxRedactDepth {
  179. return "<depth limit exceeded>"
  180. }
  181. switch v := value.(type) {
  182. case map[string]any:
  183. out := make(map[string]any, len(v))
  184. for k, val := range v {
  185. if isSensitiveKey(k, keys) {
  186. out[k] = "***"
  187. continue
  188. }
  189. out[k] = redactValueWithDepth(val, keys, depth+1)
  190. }
  191. return out
  192. case []any:
  193. out := make([]any, len(v))
  194. for i, item := range v {
  195. out[i] = redactValueWithDepth(item, keys, depth+1)
  196. }
  197. return out
  198. default:
  199. return value
  200. }
  201. }
  202. func isSensitiveKey(key string, keys map[string]struct{}) bool {
  203. _, ok := keys[normalizeKey(key)]
  204. return ok
  205. }
  206. func normalizeKey(key string) string {
  207. return strings.ToLower(strings.TrimSpace(key))
  208. }