pii_rules.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. package helper
  2. import (
  3. "strings"
  4. )
  5. // MaskChar 是脱敏时使用的掩码字符,默认为 '*'。
  6. // 抽为包级变量以便在需要时全局自定义(例如改为 '•' 或 '#')。
  7. var MaskChar = "*"
  8. // MaskKind 表示某个字段应采用的脱敏方式。
  9. type MaskKind int
  10. const (
  11. // MaskFull 完全脱敏:整个值替换为固定长度的掩码串(不泄露长度)。
  12. // 适用于手机号、完整地址串、经纬度、IMEI/IMSI/MAC/IP 等。
  13. MaskFull MaskKind = iota
  14. // MaskName 姓名脱敏:保留首字符,其余以掩码填充。
  15. MaskName
  16. // MaskEmail 邮箱脱敏:保留本地部分首字符与完整域名。
  17. MaskEmail
  18. // MaskAccount 账号脱敏:保留前 2 后 2,中间掩码。
  19. MaskAccount
  20. )
  21. // piiFieldRules 是敏感字段名到脱敏方式的映射。
  22. // 匹配时统一将 JSON key 转为小写后查表(大小写不敏感精确匹配)。
  23. //
  24. // 明确不脱敏(保留原值)的字段(不出现在此表中即视为保留):
  25. // - iccid / giccid:SIM 卡业务主键,作为查询标识符保留
  26. // - city / province / country:行政区字段,地址保留到城市级
  27. // - sn / giccid 等设备/业务标识
  28. var piiFieldRules = map[string]MaskKind{
  29. // 手机号 —— 完全脱敏
  30. "contactnumber": MaskFull,
  31. "financiercontractnumber": MaskFull,
  32. "msisdn": MaskFull,
  33. "phone": MaskFull,
  34. "mobile": MaskFull,
  35. "tel": MaskFull,
  36. // 姓名 —— 保留首字符
  37. "username": MaskName,
  38. "firstname": MaskName,
  39. "lastname": MaskName,
  40. "accountname": MaskName,
  41. "financiername": MaskName,
  42. "display_name": MaskName,
  43. "displayname": MaskName,
  44. // 邮箱
  45. "email": MaskEmail,
  46. // 完整地址串 —— 完全脱敏(行政区字段 city/province/country 不在表中,保留)
  47. "address": MaskFull,
  48. "deviceaddress": MaskFull,
  49. "addresssecondlast": MaskFull,
  50. "gpsaddress": MaskFull,
  51. // 经纬度 —— 完全脱敏
  52. "longitude": MaskFull,
  53. "latitude": MaskFull,
  54. "lng": MaskFull,
  55. "lat": MaskFull,
  56. // 账号 —— 前2后2
  57. "account": MaskAccount,
  58. "installeraccount": MaskAccount,
  59. // 设备/网络标识 —— 完全脱敏(iccid/giccid 保留,不在表中)
  60. "imei": MaskFull,
  61. "imsi": MaskFull,
  62. "mac": MaskFull,
  63. "deviceip": MaskFull,
  64. "gatewayip": MaskFull,
  65. }
  66. // fullMask 固定长度掩码串,用于 MaskFull,避免通过长度泄露原值信息。
  67. const fullMaskLen = 6
  68. // maskKindFor 返回字段名对应的脱敏方式;ok 为 false 表示该字段无需脱敏。
  69. func maskKindFor(key string) (MaskKind, bool) {
  70. k, ok := piiFieldRules[strings.ToLower(key)]
  71. return k, ok
  72. }
  73. // applyMaskString 对字符串值按指定方式脱敏。
  74. func applyMaskString(kind MaskKind, s string) string {
  75. switch kind {
  76. case MaskName:
  77. return maskName(s)
  78. case MaskEmail:
  79. return maskEmail(s)
  80. case MaskAccount:
  81. return maskAccount(s)
  82. case MaskFull:
  83. fallthrough
  84. default:
  85. return maskFull()
  86. }
  87. }
  88. // maskFull 返回固定长度的掩码串。
  89. func maskFull() string {
  90. return strings.Repeat(MaskChar, fullMaskLen)
  91. }
  92. // maskName 保留首字符(按 rune),其余固定 3 个掩码字符。
  93. // 空字符串返回空。
  94. func maskName(s string) string {
  95. if s == "" {
  96. return s
  97. }
  98. r := []rune(s)
  99. return string(r[0]) + strings.Repeat(MaskChar, 3)
  100. }
  101. // maskEmail 保留本地部分首字符 + 掩码 + 完整域名。
  102. // 非法邮箱(无 @)退化为完全脱敏。
  103. func maskEmail(s string) string {
  104. if s == "" {
  105. return s
  106. }
  107. at := strings.LastIndex(s, "@")
  108. if at <= 0 || at == len(s)-1 {
  109. return maskFull()
  110. }
  111. local := s[:at]
  112. domain := s[at:] // 含 @
  113. lr := []rune(local)
  114. return string(lr[0]) + strings.Repeat(MaskChar, 3) + domain
  115. }
  116. // maskAccount 保留前 2 后 2,中间固定掩码;长度不足 5 时完全脱敏。
  117. func maskAccount(s string) string {
  118. r := []rune(s)
  119. if len(r) < 5 {
  120. return maskFull()
  121. }
  122. return string(r[:2]) + strings.Repeat(MaskChar, 3) + string(r[len(r)-2:])
  123. }