【架构实战】数据脱敏与隐私保护:合规是底线
## 一、日志里打印了用户手机号,被安全部门约谈 2021年,安全部门扫描发现我们的日志里明文打印了用户手机号和身份证号。 根据《个人信息保护法》,这属于违规行为。我们被要求限期整改,否则面临罚款。 从那以后,数据脱敏成了我们的必修课。 --- ## 二、脱敏规则 ```java /** * 脱敏工具类 */ public class MaskUtils { /** 手机号脱敏:138****1234 */ public static String maskPhone(String phone) { if (phone == null || phone.length() ) 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管理 | **血的教训:** > 数据安全不是可选项。一次数据泄露的代价,远超安全建设的成本。 **思考题:** 你的系统做了哪些数据脱敏措施? --- *个人观点,仅供参考*