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

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

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管理

血的教训:

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

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


个人观点,仅供参考

相关推荐
GISer_Jing1 小时前
LangChain 核心架构深度解析:从设计哲学到工程实践
架构·langchain
dengyuezhe80601 小时前
《C++ 异常机制与智能指针:从原理到实现》
android·java·c++
于指尖飞舞1 小时前
java后端面试题(常用集合极简)
java·开发语言·面试
国科安芯1 小时前
基于AS32S601ZIT2型抗辐照MCU的商业航天卫星姿态确定与控制系统研究
单片机·嵌入式硬件·安全·fpga开发·架构·risc-v
我星期八休息1 小时前
Linux系统编程—mmap文件映射
java·linux·运维·服务器·数据库·mysql·spring
wanghowie1 小时前
35. 从AI客服到AI运营助手:Workflow、Single Agent、Multi-Agent、Agent Native 的架构选型实践
大数据·人工智能·架构
phltxy2 小时前
Spring AI 智能咨询系统综合实战
java·人工智能·spring
稷下元歌2 小时前
python核心基础,这关于基于Moveltg加 Ros2实战Python编程基础实课
开发语言·python