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

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

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

血的教训:

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

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


个人观点,仅供参考

相关推荐
可乐ea2 分钟前
【Java八股|第10篇】Java 中的包装类和自动拆装箱
java·面试题·包装类·java八股
zfoo-framework12 分钟前
mongo最佳实战(from mongo中文社区)
java
大气的小蜜蜂22 分钟前
基于Python+Django的健身房管理系统实现:核心亮点全流程解析
开发语言·python·django
天空'之城25 分钟前
Linux 系统编程 04:进程基础
linux·开发语言·进程基础
2zcode40 分钟前
免费开源项目文档:基于MATLAB图像处理的药片检测与计数系统设计与实现
开发语言·图像处理·matlab
凡泰AI1 小时前
从个人用AI到企业用AI,如何为企业部署一套私有化Agent智能体运行时,将AI变成企业的基础设施
人工智能·ai·架构·agent·cio
深盾科技_Virbox1 小时前
加密狗授权能力选型:从授权模型到全生命周期管理
java·网络·数据库
charlie1145141911 小时前
Cinux: 加载第一个内核:从 bootloader 跳进 C++
linux·开发语言·c++·嵌入式
柒和远方1 小时前
Phase 7.4 学习博客:为什么多 API 项目需要 Swagger / OpenAPI
前端·后端·架构
mONESY1 小时前
AI Loop 自动化工程实践,放弃手工调 Prompt,循环才是标准答案!
架构