数据脱敏是怎么做的

数据脱敏是怎么做的

什么是数据脱敏?

数据脱敏就是对敏感数据(手机号、身份证、银行卡、地址等)进行不可逆或可逆的变形处理,在不影响业务正常使用的前提下,防止数据泄露。

核心三原则:

  • 不可逆性:脱敏后的数据无法还原为原始数据(静态脱敏)
  • 一致性:相同原始数据脱敏后结果一致(保证统计分析可用)
  • 可用性:脱敏后的数据保留原有格式和业务特征(如手机号保留前 3 后 4)

常用脱敏算法对比📊

算法类型 实现原理 适用场景 优点 缺点
掩码脱敏 保留部分字符,其余用 * 代替 前端展示、日志打印 最简单、性能最好 安全性最低,易被暴力破解
替换脱敏 用随机字符 / 字典替换原始数据 测试环境、数据分析 保留数据格式和业务逻辑 字典维护成本高
哈希脱敏 MD5/SHA256 + 盐值计算 用户密码、唯一标识 不可逆,安全性高 相同输入输出相同,易被彩虹表攻击
加密脱敏 AES/RSA 对称 / 非对称加密 传输过程、需要还原的场景 可还原,安全性最高 性能最差,密钥管理复杂
截断脱敏 只保留数据的前 N 位或后 N 位 非核心字段展示 简单高效 丢失数据完整性
洗牌脱敏 打乱数据内部字符顺序 地址、姓名 保留数据长度和字符集 可能保留部分语义信息

关于这个问题的底层原理和更多实战细节,我整理了一份《大厂面试手册》,包含大厂高频面试题、源码解析和性能调优案例。

关注公众号【Rain的Java大神之路】,回复"Java"即可免费领取,持续更新中。


企业级分层脱敏架构🏗️

我们团队采用的是分层脱敏方案,在数据流转的各个环节都做防护,避免单点失效。

各层具体实现:

  1. 前端展示层:前端组件统一处理,只展示脱敏后的数据,原始数据不传到前端
  2. 接口层:使用 Spring AOP 拦截所有接口响应,对标注了@Sensitive注解的字段自动脱敏
  3. 日志层 :重写 logback 的MessageConverter,自动过滤日志中的敏感数据
  4. 数据访问层 :基于 MyBatis 的Interceptor实现查询结果的自动脱敏和插入前的加密
  5. 数据库层:生产库定期导出到测试库时,使用脚本批量静态脱敏

关键技术难点与解决方案⚠️

1.动态脱敏问题

  • 问题:不同角色看到的脱敏程度不同(管理员看完整数据,普通员工看脱敏数据)
  • 解决方案:基于 Spring Security 的权限体系,在 AOP 中根据当前用户角色动态选择脱敏策略

2.脱敏与业务解耦

  • 问题:如果在业务代码中硬编码脱敏逻辑,会导致代码混乱且难以维护
  • 解决方案:自定义@Sensitive注解,配合 AOP 实现声明式脱敏,业务代码无感知
java 复制代码
// 自定义脱敏注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
    SensitiveType value(); // 脱敏类型:PHONE, ID_CARD, BANK_CARD等
}

// 使用示例
public class User {
    private Long id;
    
    @Sensitive(SensitiveType.PHONE)
    private String phone;
    
    @Sensitive(SensitiveType.ID_CARD)
    private String idCard;
}

3.大数据场景下的脱敏

  • 问题:Hive、Spark 等大数据平台处理海量数据时,传统脱敏方式性能不足
  • 解决方案:使用 UDF 函数实现分布式脱敏,在数据写入时就完成脱敏处理

总结✅

数据脱敏不是单一技术,而是一个体系化的工程。最好的实践是:

  • 静态脱敏用于测试环境和数据分析
  • 动态脱敏用于生产环境的实时访问
  • 结合权限控制,实现细粒度的数据保护
  • 定期审计脱敏效果,及时发现漏洞

这样既保证了数据安全,又不会对业务性能造成太大影响。

核心代码实现(带技术亮点✨)

我们采用声明式 + 策略模式的架构,实现了业务代码零侵入的脱敏方案,支持动态扩展脱敏算法。

1. 基础定义层(技术亮点:开闭原则设计)

java 复制代码
/**
 * 脱敏类型枚举
 * 新增脱敏类型只需在这里添加,无需修改原有业务代码
 */
public enum SensitiveType {
    PHONE,       // 手机号
    ID_CARD,     // 身份证
    BANK_CARD,   // 银行卡
    EMAIL,       // 邮箱
    NAME,        // 姓名
    ADDRESS,     // 地址
    CUSTOM       // 自定义策略
}

/**
 * 脱敏策略接口(策略模式核心)
 * 所有脱敏算法都实现此接口,支持Spring自动注入
 */
public interface SensitiveStrategy {
    String desensitize(String original);
}

2. 默认脱敏策略实现

java 复制代码
/**
 * 手机号脱敏策略
 * 保留前3后4位:138****1234
 */
@Component
public class PhoneSensitiveStrategy implements SensitiveStrategy {
    @Override
    public String desensitize(String original) {
        if (original == null || original.length() != 11) {
            return original;
        }
        return original.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }
}

/**
 * 身份证脱敏策略
 * 保留前6后4位:110101********1234
 */
@Component
public class IdCardSensitiveStrategy implements SensitiveStrategy {
    @Override
    public String desensitize(String original) {
        if (original == null || original.length() != 18) {
            return original;
        }
        return original.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
    }
}

3. 自定义脱敏注解

java 复制代码
/**
 * 敏感字段注解
 * 标注在实体类字段上,自动触发脱敏
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
    SensitiveType value() default SensitiveType.CUSTOM;
    
    // 支持自定义脱敏策略类(扩展点)
    Class<? extends SensitiveStrategy> strategy() default SensitiveStrategy.class;
}

4. Spring AOP 动态脱敏切面(核心技术亮点🔥)

java 复制代码
/**
 * 全局响应脱敏切面
 * 技术亮点:
 * 1. 结合Spring Security实现基于角色的动态脱敏
 * 2. 递归处理嵌套对象和集合,支持复杂数据结构
 * 3. 缓存脱敏策略实例,避免重复反射创建
 */
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SensitiveAspect {
    @Autowired
    private ApplicationContext applicationContext;
    
    // 缓存脱敏策略实例
    private final Map<Class<? extends SensitiveStrategy>, SensitiveStrategy> strategyCache = new ConcurrentHashMap<>();
    
    @Around("@annotation(org.springframework.web.bind.annotation.ResponseBody)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();
        if (result == null) {
            return null;
        }
        
        // 获取当前用户角色
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        boolean isAdmin = auth != null && auth.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
        
        // 管理员不脱敏
        if (isAdmin) {
            return result;
        }
        
        // 递归脱敏处理
        desensitizeObject(result);
        return result;
    }
    
    private void desensitizeObject(Object obj) throws IllegalAccessException {
        if (obj == null || obj.getClass().isPrimitive() || obj instanceof String) {
            return;
        }
        
        // 处理集合
        if (obj instanceof Collection<?>) {
            for (Object item : (Collection<?>) obj) {
                desensitizeObject(item);
            }
            return;
        }
        
        // 处理实体类字段
        for (Field field : obj.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            Sensitive sensitive = field.getAnnotation(Sensitive.class);
            
            if (sensitive != null && field.getType() == String.class) {
                String original = (String) field.get(obj);
                String desensitized = getStrategy(sensitive).desensitize(original);
                field.set(obj, desensitized);
            } else {
                // 递归处理嵌套对象
                desensitizeObject(field.get(obj));
            }
        }
    }
    
    // 从缓存获取策略实例
    private SensitiveStrategy getStrategy(Sensitive sensitive) {
        Class<? extends SensitiveStrategy> strategyClass = sensitive.strategy();
        if (strategyClass == SensitiveStrategy.class) {
            // 根据类型获取默认策略
            String beanName = sensitive.value().name().toLowerCase() + "SensitiveStrategy";
            return applicationContext.getBean(beanName, SensitiveStrategy.class);
        }
        return strategyCache.computeIfAbsent(strategyClass, applicationContext::getBean);
    }
}

5. Logback 全局日志脱敏(容易忽略的安全点⚠️)

java 复制代码
/**
 * 日志脱敏转换器
 * 技术亮点:全局统一处理,避免业务代码中手动过滤敏感数据
 */
public class SensitiveLogConverter extends ClassicConverter {
    // 匹配手机号、身份证、银行卡的正则表达式
    private static final Pattern[] PATTERNS = {
        Pattern.compile("1[3-9]\\d{9}"),
        Pattern.compile("\\d{17}[\\dXx]"),
        Pattern.compile("\\d{16,19}")
    };
    
    @Override
    public String convert(ILoggingEvent event) {
        String message = event.getFormattedMessage();
        for (Pattern pattern : PATTERNS) {
            Matcher matcher = pattern.matcher(message);
            StringBuffer sb = new StringBuffer();
            while (matcher.find()) {
                // 统一替换为****
                matcher.appendReplacement(sb, "****");
            }
            matcher.appendTail(sb);
            message = sb.toString();
        }
        return message;
    }
}

技术难点与解决方案汇总📋

技术难点 问题影响 解决方案 核心技术亮点
动态权限脱敏 不同角色看到不同脱敏程度的数据,硬编码会导致代码混乱 基于 Spring Security + AOP,在切面中判断用户角色动态选择脱敏策略 角色与脱敏逻辑解耦,支持细粒度权限控制
脱敏后数据关联问题 哈希脱敏后,相同原始数据需要保持相同结果才能进行关联查询 使用固定盐值 + SHA256哈希,保证一致性;需要还原的场景使用 AES 可逆加密 兼顾安全性和业务可用性
批量数据脱敏性能 千万级数据批量脱敏时,单线程处理耗时过长 使用 MyBatis 批处理 + 多线程池,大数据场景使用 Spark UDF 分布式脱敏 性能提升 10 倍以上,支持 TB 级数据处理
密钥管理问题 加密脱敏的密钥硬编码在代码中,存在泄露风险 使用阿里云 KMS/HashiCorp Vault 管理密钥,实现密钥自动轮换 符合等保三级要求,密钥生命周期管理
第三方接口数据脱敏 调用第三方接口时,需要传递部分敏感数据 建立统一的 API 网关,在网关层对出入参进行自动脱敏 / 加密 业务代码无感知,统一管控所有外部调用
数据导出脱敏 Excel/PDF 导出时容易泄露完整敏感数据 重写 POI 导出工具类,自动识别 @Sensitive 注解并脱敏 与现有导出逻辑无缝集成
脱敏规则变更 业务变更需要修改脱敏规则时,需要重新发布代码 将脱敏规则配置在 Nacos 配置中心,支持热更新 无需重启服务,实时生效
嵌套对象脱敏 复杂数据结构(如订单包含用户、地址)容易遗漏脱敏 在 AOP 切面中递归遍历所有字段,处理嵌套对象和集合 支持任意复杂的数据结构

面试加分总结💯

数据脱敏的核心不是某一个算法,而是全链路的防护体系。我们的方案做到了:

  • 业务代码零侵入:通过注解和 AOP 实现
  • 可扩展性强:新增脱敏算法只需实现一个接口
  • 动态灵活:支持基于角色的细粒度脱敏
  • 性能优秀:缓存策略实例,支持分布式批量处理

最重要的是,我们建立了定期的脱敏效果审计机制,避免出现 "假脱敏" 的情况。

真实面试模拟

真实面试模拟

👨‍💼 面试官:

同学你好,今天场景设计题我们聊个实际的------你在项目中数据脱敏是怎么做的? 不用面面俱到,把你认为最关键的几层设计讲清楚就行。

🧑‍💻 候选人:

好的面试官。我们主要是按展示、日志、存储、导出四层来分别处理的,先给您看个总览图,心里有个谱:

text 复制代码
┌─────────────────────────────────────────────────────────┐
│                    数据脱敏体系                           │
├─────────────┬──────────────┬──────────────┬─────────────┤
│  展示层      │   日志层      │  传输/存储层   │  数据导出层   │
│  (动态脱敏)  │  (日志脱敏)   │  (静态脱敏)    │  (静态脱敏)   │
├─────────────┼──────────────┼──────────────┼─────────────┤
│ 注解+AOP    │ logback       │ 数据库字段    │ EasyExcel    │
│ Jackson序列化│ 自定义Converter│ 加密存储(AES) │ 脱敏注解     │
│ 网关脱敏    │ 基于正则替换   │ 脱敏视图      │ 异步脱敏处理  │
└─────────────┴──────────────┴──────────────┴─────────────┘

👨‍💼 面试官:

图很清晰 👍 先说说展示层动态脱敏 吧,你们是怎么让前端看到的接口 JSON 里手机号直接变成 138****5678 的?

🧑‍💻 候选人:

这块核心用的是 Jackson 自定义序列化 + 注解,对业务代码侵入特别小。

java 复制代码
// ① 自定义脱敏注解
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerialize.class)
public @interface Sensitive {
    SensitiveType type(); // 枚举:MOBILE, ID_CARD, NAME...
}
// ② 自定义序列化器,根据type选择脱敏策略
public class SensitiveSerialize extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider provider) {
        Sensitive anno = provider.getAttribute("sensitive_anno");
        gen.writeString(SensitiveStrategy.get(anno.type()).desensitize(value));
    }
}

VO 字段上加个注解就行:

java 复制代码
public class UserVO {
    @Sensitive(type = SensitiveType.MOBILE)
    private String mobile;
}

💡 注意点:高并发下别在策略里临时编译正则,提前缓存 Pattern,压测 QPS 影响不超过 2%。

👨‍💼 面试官:

如果系统里字段特别多,总不能一个个手加注解吧?

🧑‍💻 候选人:

对,所以我们还会做"敏感数据自动发现",用正则扫描元数据打标,然后通过配置中心动态驱动序列化,做到新接口上线零改动。

👨‍💼 面试官:

好,那日志脱敏你们怎么防止把身份证打到 ELK 里?

🧑‍💻 候选人:

全局重写了 logback 的 MessageConverter,匹配敏感信息替换,开发无感知。

java 复制代码
public class SensitiveLogConverter extends MessageConverter {
    @Override
    public String convert(ILoggingEvent event) {
        String msg = super.convert(event);
        return SensitiveLogUtil.desensitize(msg); // 链式正则替换
    }
}

logback.xml 配置一行:

xml 复制代码
<conversionRule conversionWord="msg" 
    converterClass="com.xxx.SensitiveLogConverter" />

🛡️ 还把 MDC 里的 userId 做了哈希处理,这样就算日志泄露也关联不到真人。

👨‍💼 面试官:

日志层过了,那数据库存储这层怎么办?有些场景还得能还原对吧。

🧑‍💻 候选人:

对,实名数据需要可逆,我们用应用层透明加解密,MyBatis 的 TypeHandler 搞定:

java 复制代码
public class EncryptTypeHandler extends BaseTypeHandler<String> {
    public void setNonNullParameter(PreparedStatement ps, int i, String param) {
        ps.setString(i, AESUtil.encrypt(param));
    }
    // 查询时自动解密...
}

密钥走 KMS,不落地,权限隔离。不可逆的查询条件用 HASH 关联,DBA 还会建脱敏视图给低权限应用。

👨‍💼 面试官:

运营导出 Excel 这种场景呢?不同角色看到的数据还不一样。

🧑‍💻 候选人:

我们用策略工厂 + EasyExcel 拦截器动态处理。

java 复制代码
public class SensitiveStrategyFactory {
    public static SensitiveStrategy get(Role role, SensitiveType type) {
        if (role == Role.ADMIN) return NoOpStrategy.INSTANCE; // 管理员看明文
        return registry.get(type); // 普通运营按类型脱敏
    }
}

大数据量导出就异步任务:查询 → 逐行脱敏 → 上传 OSS → 通知下载,避免超时。

👨‍💼 面试官:

最后帮我总结下,这套设计的核心原则是什么?

🧑‍💻 候选人:

三个点:

  • 低侵入:能用自动化、配置驱动的绝不手写;
  • 分层治理:展示、日志、存储、导出各有专门方案;
  • 可追溯:脱敏操作留痕,能知道谁看了什么数据。

这样既能过安全审计,开发体验也不差 😄

👨‍💼 面试官:

讲得很落地,没大问题。那你们落地过程中遇到过什么坑吗?

🧑‍💻 候选人:

比如日志脱敏刚开始把接口参数里的 JSON 手机号替换了,但没考虑到 MDC 里的原始值,导致链路追踪里漏了,后面补上了 MDC 过滤;还有动态脱敏注解在 Feign 调用时序列化不生效,需要把序列化器也注册到 Feign 的编解码器里。

👨‍💼 面试官:

刚才几层设计思路很清晰,那能不能把你觉得最有技术亮点的代码 贴出来看看?另外,这个脱敏体系落地时,技术难点有哪些?你们是怎么解决的?

🧑‍💻 候选人:

没问题,我挑三段核心代码,然后说四个关键难点和方案。

🔥 核心亮点代码

1. 展示层:可缓存策略的序列化器(解决性能)

java 复制代码
public class SensitiveSerialize extends JsonSerializer<String> 
                                  implements ContextualSerializer {
    // 缓存编译好的策略函数,避免每次序列化都查找
    private static final Map<SensitiveType, Function<String, String>> STRATEGY_CACHE = 
        new ConcurrentHashMap<>();

    private SensitiveType type;

    @Override
    public void serialize(String value, JsonGenerator gen, 
                          SerializerProvider provider) throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }
        Function<String, String> func = STRATEGY_CACHE.computeIfAbsent(type,
            t -> SensitiveStrategyRegistry.get(t)); // 策略注册表
        gen.writeString(func.apply(value));
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov,
                                              BeanProperty property) {
        Sensitive anno = property.getAnnotation(Sensitive.class);
        if (anno != null) {
            SensitiveSerialize serializer = new SensitiveSerialize();
            serializer.type = anno.type();
            return serializer;
        }
        return this;
    }
}

💡 亮点:通过 ConcurrentHashMap 缓存策略函数,高并发下只做一次策略匹配,压测时序列化几乎无额外开销。

2. 存储层:MyBatis 透明加解密 TypeHandler

java 复制代码
@MappedTypes(String.class)
public class AESEncryptTypeHandler extends BaseTypeHandler<String> {
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                   String param, JdbcType jdbcType) 
                                   throws SQLException {
        // 加密后入库,密钥从KMS获取的本地缓存取
        ps.setString(i, AESUtils.encrypt(param, KeyHolder.get()));
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) 
                                    throws SQLException {
        String encrypted = rs.getString(columnName);
        return encrypted != null ? AESUtils.decrypt(encrypted, KeyHolder.get()) : null;
    }
    // 其他 getNullableResult 重载...
}

💡 亮点:结合 KeyHolder 内存缓存 KMS 密钥,避免每次加解密都远程调用;业务代码完全不用改,只需在 mapper.xml 指定 typeHandler

3. 敏感数据自动发现扫描器(解决字段打标难题)

java 复制代码
@Component
public class SensitiveDataScanner {
    
    // 内置常见敏感列正则
    private static final Map<String, Pattern> RULES = Map.of(
        "mobile", Pattern.compile(".*(phone|mobile|tel).*", Pattern.CASE_INSENSITIVE),
        "id_card", Pattern.compile(".*(id_card|identity|身份证).*", Pattern.CASE_INSENSITIVE)
        // ... 更多规则
    );

    @Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点扫描
    public void scanAndTag() {
        List<TableMetadata> tables = metadataService.getAllTables();
        for (TableMetadata table : tables) {
            for (ColumnMeta col : table.getColumns()) {
                SensitiveType type = matchType(col.getName(), col.getComment());
                if (type != null) {
                    tagService.saveOrUpdate(table.getName(), col.getName(), type);
                }
            }
        }
    }

    private SensitiveType matchType(String colName, String comment) {
        for (Map.Entry<String, Pattern> entry : RULES.entrySet()) {
            if (entry.getValue().matcher(colName).matches() ||
                (comment != null && entry.getValue().matcher(comment).matches())) {
                return SensitiveType.valueOf(entry.getKey().toUpperCase());
            }
        }
        return null;
    }
}

💡 亮点:用定时任务自动打标,新字段上线一天内自动纳入脱敏体系,彻底解决人工加注解遗漏的痛点。

🧨 技术难点与解决方案

难点 问题描述 解决方案
性能开销 日志/序列化脱敏频繁正则匹配,拖慢接口 ① 策略函数缓存;② 正则 Pattern 预编译并缓存;③ 网关层统一脱敏分流
注解跨服务失效 Feign/RPC 调用时 Jackson 脱敏序列化器不生效 让 API 返回的 VO 实现统一脱敏接口,在 Feign 编解码器里注入同一个序列化器
密钥安全与轮换 数据库加密密钥硬编码、泄露风险、无法轮换 引入 KMS,密钥不落地,应用启动时拉到内存(定期刷新),按需加解密且权限最小化
日志脱敏遗漏 只脱敏了日志体,MDC、异常堆栈中的敏感数据仍明文化 扩展 MessageConverter 同时处理格式化消息和 MDC 值;异常信息在全局异常处理器中二次脱敏

难点解决流程图(简化)

text 复制代码
┌─────────────┐      ┌──────────────────┐      ┌───────────────┐
│ 数据请求到达  │────▶│ 网关层统一脱敏(可选) │────▶│ 业务服务       │
└─────────────┘      └──────────────────┘      └───────┬───────┘
                                                        │
                         ┌───────────────────────────────┤
                         ▼                               ▼
              ┌─────────────────┐            ┌──────────────────┐
              │ Jackson序列化    │            │ 日志脱敏Converter │
              │ (缓存策略+正则)  │            │ (全局+MDC)       │
              └─────────────────┘            └──────────────────┘
                         │                               │
                         ▼                               ▼
              ┌─────────────────┐            ┌──────────────────┐
              │ 返回脱敏后JSON   │            │ 输出安全日志      │
              └─────────────────┘            └──────────────────┘

👨‍💼 面试官:

很好,把细节和坑都讲清楚了。特别是性能缓存和自动发现这两个点,确实能看出实际落地的深度 👍 那今天数据脱敏这块我们就聊到这里。

🧑‍💻 候选人:

谢谢面试官 😄


如果本文对你有帮助,欢迎关注我的公众号【Rain的Java大神之路】。

专注 Java 面试、源码、高并发实战,回复"Java"领取《大厂面试手册》,持续更新。

相关推荐
Rain的Java大神实战圈1 天前
对加密的手机号如何进行模糊查询
场景设计题
Rain的Java大神实战圈2 天前
如何自定义一个MyBatis插件
场景设计题
Rain的Java大神实战圈3 天前
高并发下的抽奖系统:不只是扣库存,还要防作弊与风控
场景设计题
Rain的Java大神实战圈6 天前
高并发下的秒杀系统架构全景:从单体到云原生的演进之路
场景设计题
Rain的Java大神实战圈7 天前
Spring事务失效的场景有哪些
场景设计题
Rain的Java大神实战圈8 天前
高并发下的排行榜系统:如何用Redis快速实现千万用户实时排名
场景设计题
Rain的Java大神实战圈9 天前
手写一个Spring容器
场景设计题
Rain的Java大神实战圈13 天前
JavaWeb开发如何解决跨域问题
场景设计题
Rain的Java大神实战圈14 天前
高并发下的热点账户余额扣减:Redis+Lua脚本实现无锁记账
场景设计题