【架构实战】数据脱敏与隐私保护:合规是底线

一、日志里打印了用户手机号,被安全部门约谈

2021年,安全部门扫描发现我们的日志里明文打印了用户手机号和身份证号。

根据《个人信息保护法》,这属于违规行为。我们被要求限期整改,否则面临罚款。

从那以后,数据脱敏成了我们的必修课。


二、脱敏规则

java 复制代码
/**
 * 脱敏工具类
 */
public class MaskUtils {
    
    /** 手机号脱敏:138****1234 */
    public static String maskPhone(String phone) {
        if (phone == null || phone.length() < 7) return phone;
        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
    }
    
    /** 身份证脱敏:3301****1234 */
    public static String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() < 8) return idCard;
        return idCard.substring(0, 4) + "****" + idCard.substring(idCard.length() - 4);
    }
    
    /** 邮箱脱敏:t***@example.com */
    public static String maskEmail(String email) {
        if (email == null || !email.contains("@")) return email;
        String[] parts = email.split("@");
        return parts[0].charAt(0) + "***@" + parts[1];
    }
    
    /** 姓名脱敏:张** */
    public static String maskName(String name) {
        if (name == null || name.length() < 2) return name;
        return name.charAt(0) + "**";
    }
    
    /** 银行卡脱敏:**** **** **** 1234 */
    public static String maskBankCard(String cardNo) {
        if (cardNo == null || cardNo.length() < 4) return cardNo;
        return "**** **** **** " + cardNo.substring(cardNo.length() - 4);
    }
    
    /** 地址脱敏:浙江省杭州市**** */
    public static String maskAddress(String address) {
        if (address == null || address.length() < 6) return address;
        return address.substring(0, 6) + "****";
    }
}

2.1 MyBatis脱敏拦截器

java 复制代码
/**
 * 查询结果自动脱敏
 */
@Intercepts({
    @Signature(type = Executor.class, method = "query", 
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
public class DataMaskInterceptor implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object result = invocation.proceed();
        
        if (result instanceof List) {
            for (Object item : (List<?>) result) {
                maskSensitiveFields(item);
            }
        } else if (result != null) {
            maskSensitiveFields(result);
        }
        
        return result;
    }
    
    private void maskSensitiveFields(Object obj) {
        if (obj == null) return;
        
        for (Field field : obj.getClass().getDeclaredFields()) {
            Sensitive sensitive = field.getAnnotation(Sensitive.class);
            if (sensitive != null && field.getType() == String.class) {
                field.setAccessible(true);
                try {
                    String value = (String) field.get(obj);
                    if (value != null) {
                        field.set(obj, maskByType(value, sensitive.type()));
                    }
                } catch (IllegalAccessException e) {
                    // ignore
                }
            }
        }
    }
    
    private String maskByType(String value, SensitiveType type) {
        switch (type) {
            case PHONE: return MaskUtils.maskPhone(value);
            case ID_CARD: return MaskUtils.maskIdCard(value);
            case EMAIL: return MaskUtils.maskEmail(value);
            case NAME: return MaskUtils.maskName(value);
            case BANK_CARD: return MaskUtils.maskBankCard(value);
            case ADDRESS: return MaskUtils.maskAddress(value);
            default: return value;
        }
    }
}

/**
 * 敏感字段注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
    SensitiveType type();
}

// 使用
@Data
public class UserVO {
    private Long id;
    private String username;
    
    @Sensitive(type = SensitiveType.PHONE)
    private String phone;
    
    @Sensitive(type = SensitiveType.ID_CARD)
    private String idCard;
    
    @Sensitive(type = SensitiveType.EMAIL)
    private String email;
}

三、日志脱敏

java 复制代码
/**
 * 日志脱敏拦截器
 */
@Component
public class LogMaskFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                          FilterChain chain) throws IOException, ServletException {
        ContentCachingRequestWrapper wrappedRequest = 
            new ContentCachingRequestWrapper((HttpServletRequest) request);
        
        chain.doFilter(wrappedRequest, response);
        
        // 日志中记录请求体(脱敏后)
        String body = new String(wrappedRequest.getContentAsByteArray());
        String masked = maskSensitiveData(body);
        log.info("Request: uri={}, body={}", 
            ((HttpServletRequest) request).getRequestURI(), masked);
    }
    
    private String maskSensitiveData(String data) {
        // 正则匹配手机号
        data = data.replaceAll("(1[3-9]\\d)\\d{4}(\\d{4})", "$1****$2");
        // 正则匹配身份证号
        data = data.replaceAll("(\\d{4})\\d{10}(\\d{4})", "$1**********$2");
        // 正则匹配邮箱
        data = data.replaceAll("(\\w)\\w+(@\\w+\\.\\w+)", "$1***$2");
        return data;
    }
}

四、加密存储

java 复制代码
/**
 * 数据库字段加密
 */
@Component
public class FieldEncryptUtil {
    
    @Autowired
    private AesEncryptor aesEncryptor;
    
    /**
     * 加密
     */
    public String encrypt(String plaintext) {
        return aesEncryptor.encrypt(plaintext);
    }
    
    /**
     * 解密
     */
    public String decrypt(String ciphertext) {
        return aesEncryptor.decrypt(ciphertext);
    }
}

/**
 * MyBatis加密拦截器
 */
@Intercepts({
    @Signature(type = Executor.class, method = "update"),
    @Signature(type = Executor.class, method = "query")
})
@Component
public class FieldEncryptInterceptor implements Interceptor {
    
    @Autowired
    private FieldEncryptUtil encryptUtil;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 写入时加密
        if ("update".equals(invocation.getMethod().getName())) {
            encryptParams(invocation.getArgs());
        }
        
        Object result = invocation.proceed();
        
        // 查询时解密
        if ("query".equals(invocation.getMethod().getName())) {
            decryptResult(result);
        }
        
        return result;
    }
}

五、踩坑实录

坑1:脱敏了但数据库里没加密

API返回脱敏了,但数据库里是明文存储,被SQL注入后泄露。

解决:敏感字段数据库加密存储。

坑2:脱敏规则不统一

不同服务的脱敏方式不同,A服务手机号3位+4位,B服务4位+4位。

解决:统一脱敏工具类,强制使用。

坑3:日志脱敏遗漏

有些日志是第三方库打印的,没有经过我们的脱敏过滤器。

解决:Logback脱敏插件,统一处理所有日志输出。

坑4:加密后无法查询

手机号加密存储后,无法按手机号查询。

解决:存储脱敏值+加密值,用脱敏值查询;或使用可搜索加密。

坑5:密钥管理

加密密钥硬编码在代码里,不安全。

解决:密钥存储在KMS(密钥管理服务),运行时动态获取。


六、总结

数据隐私保护要点:

层面 方案
传输 HTTPS + 字段加密
存储 数据库字段加密
展示 脱敏返回
日志 自动脱敏
密钥 KMS管理

血的教训:

数据安全不是可选项。一次数据泄露的代价,远超安全建设的成本。

思考题: 你的系统做了哪些数据脱敏措施?


个人观点,仅供参考

相关推荐
是小蟹呀^9 小时前
Spring Security + JWT 面试题整理
java·jwt·springsecurity
程序员小羊!10 小时前
集团多事业部架构下数仓分层建模规范
架构
星栈独行11 小时前
翻完 Pi 源码:它和 Codex、Claude Code 有何不同
开发语言·javascript·人工智能·程序人生
spencer_tseng11 小时前
Redis + Nacos.bat
java·windows·dos
qq_4480111611 小时前
C语言中的变量和函数的定义与声明
android·c语言·开发语言
troyzhxu12 小时前
列表查询的 GraphQL —— 一行代码终结你的 if-else 地狱!
java·springboot·graphql
ARM|X86+FPGA工业主板厂家12 小时前
RK3588+FPGA+EtherCAT异构架构解析|工业场景如何同时保住AI算力与微秒级运动实时性
人工智能·fpga开发·架构
小码哥哥12 小时前
四大企业AI知识库技术架构深度对比:从设计哲学到实现差异
人工智能·架构
孫治AllenSun12 小时前
【DataX】生产环境搭建DataX集群案例
java·开发语言·jvm