Files
junhong_cmp_fiber/pkg/sanitizer/sanitizer.go
break 70e680eb0a
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s
feat(手机号资产关联): AUG26-009 手机号—资产关联、十项上限与后台解绑
- 新增成对迁移 000223(tb_phone_asset_association,含有效关系部分唯一索引与 down 守卫)与 000224(解绑导入任务表),不回填历史
- H5:need_bind_phone 三支判定(开关关闭完全短路);已有主号幂等建联;十项上限按手机号 advisory 串行化(含换绑到全新号的并发场景);换绑原子迁移与冲突整单回滚;不写遗留列
- 后台:关联列表、单项/批量解绑、CSV 导入解绑(B1–B16),超管/平台 gate + 资产数据范围复核,三态统一文案
- 读侧:卡/设备列表与详情按页一次 IN 聚合;两类导出补「关联手机号」列并保留历史表头反解兼容
- 脱敏:关联审计走独立动作/资源只写脱敏手机号;访问日志手机号类字段脱敏
- 同步主 Spec openspec/specs/phone-asset-association 并归档 AUG26-009,补齐 requirement-evidence 与入口矩阵,context-health 通过
2026-09-15 11:54:56 +08:00

133 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package sanitizer 提供 Access、Audit 与 Integration 共用的敏感字段清理能力。
package sanitizer
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"regexp"
"strings"
"unicode"
"github.com/bytedance/sonic"
)
var forbiddenFragments = []string{
"password", "passwd", "credential", "operation_password", "verification_code", "captcha",
"token", "access_token", "refresh_token", "id_token", "session_token", "sms_code", "api_key", "payment_key",
"authorization", "cookie", "secret", "private_key", "public_key",
"encoding_aes_key", "callback_token", "signature", "sign", "nonce", "media_id", "signed_url",
"private_url", "qr_content", "id_card", "identity_number",
}
var forbiddenTextPattern = regexp.MustCompile(`(?i)(bearer[[:space:]]+[a-z0-9._~+/=-]{8,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|(?:access_token|refresh_token|id_token|session_token|authorization|cookie|secret|private_key|api_key|payment_key|signed_url|x-amz-signature|x-amz-credential)[[:space:]]*[=:][[:space:]]*[^&[:space:]]+)`)
// IsForbiddenField 判断字段是否禁止进入普通日志、审计或外部交互摘要。
func IsForbiddenField(key string) bool {
normalized := normalizeFieldName(key)
if normalized == "credentials_configured" || normalized == "token_present" {
return false
}
for _, fragment := range forbiddenFragments {
if strings.Contains(normalized, fragment) {
return true
}
}
if strings.HasSuffix(normalized, "_key") || normalized == "key" || strings.HasSuffix(normalized, "_url") || normalized == "url" {
return true
}
return false
}
func normalizeFieldName(key string) string {
var normalized strings.Builder
for index, char := range key {
if unicode.IsUpper(char) && index > 0 {
normalized.WriteByte('_')
}
if char == '-' || char == '.' {
normalized.WriteByte('_')
continue
}
normalized.WriteRune(unicode.ToLower(char))
}
return normalized.String()
}
// MarshalSummary 递归删除禁止字段并返回 sonic 编码的安全 JSON。
func MarshalSummary(value any) ([]byte, error) {
if value == nil {
return nil, nil
}
encoded, err := sonic.Marshal(value)
if err != nil {
return nil, err
}
var normalized any
if err := sonic.Unmarshal(encoded, &normalized); err != nil {
return nil, err
}
RemoveForbiddenFields(normalized)
return sonic.Marshal(normalized)
}
// RemoveForbiddenFields 原地递归删除 Map 或数组中的禁止字段。
func RemoveForbiddenFields(value any) {
switch typed := value.(type) {
case map[string]any:
for key, item := range typed {
if IsForbiddenField(key) {
delete(typed, key)
continue
}
if text, ok := item.(string); ok {
typed[key] = SanitizeText(text)
continue
}
RemoveForbiddenFields(item)
}
case []any:
for index, item := range typed {
if text, ok := item.(string); ok {
typed[index] = SanitizeText(text)
continue
}
RemoveForbiddenFields(item)
}
}
}
// SanitizeText 将疑似包含安全凭据的文本转换为不可逆摘要。
func SanitizeText(value string) string {
if !forbiddenTextPattern.MatchString(value) {
return value
}
return TextSummary(value)
}
// 手机号脱敏保留位数:前 3 位与后 4 位。
const (
phoneMaskedKeepPrefix = 3
phoneMaskedKeepSuffix = 4
)
// MaskPhone 生成脱敏手机号,仅保留前 3 位与后 4 位。
// 审计、日志与错误文案一律使用本函数的结果;字段名脱敏清单不含 phone
// 因此不能依赖 RemoveForbiddenFields 自动脱敏,必须显式调用。
func MaskPhone(phone string) string {
phone = strings.TrimSpace(phone)
if len(phone) < phoneMaskedKeepPrefix+phoneMaskedKeepSuffix {
return ""
}
return phone[:phoneMaskedKeepPrefix] + "****" + phone[len(phone)-phoneMaskedKeepSuffix:]
}
// TextSummary 将不可信外部文本转换为不可逆大小和哈希摘要。
func TextSummary(value string) string {
if value == "" {
return ""
}
sum := sha256.Sum256([]byte(value))
return fmt.Sprintf("外部文本摘要 bytes=%d sha256=%s", len(value), hex.EncodeToString(sum[:8]))
}