数据脱敏是怎么做的
数据脱敏是怎么做的
面试官您好,关于数据脱敏,我会从核心定义、常用算法、企业级分层实现、关键难点四个维度来回答,这也是我们团队实际落地的完整方案。
什么是数据脱敏?
数据脱敏就是对敏感数据(手机号、身份证、银行卡、地址等)进行不可逆或可逆的变形处理,在不影响业务正常使用的前提下,防止数据泄露。
核心三原则:
- 不可逆性:脱敏后的数据无法还原为原始数据(静态脱敏)
- 一致性:相同原始数据脱敏后结果一致(保证统计分析可用)
- 可用性:脱敏后的数据保留原有格式和业务特征(如手机号保留前 3 后 4)
常用脱敏算法对比📊
| 算法类型 | 实现原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 掩码脱敏 | 保留部分字符,其余用 * 代替 | 前端展示、日志打印 | 最简单、性能最好 | 安全性最低,易被暴力破解 |
| 替换脱敏 | 用随机字符 / 字典替换原始数据 | 测试环境、数据分析 | 保留数据格式和业务逻辑 | 字典维护成本高 |
| 哈希脱敏 | MD5/SHA256 + 盐值计算 | 用户密码、唯一标识 | 不可逆,安全性高 | 相同输入输出相同,易被彩虹表攻击 |
| 加密脱敏 | AES/RSA 对称 / 非对称加密 | 传输过程、需要还原的场景 | 可还原,安全性最高 | 性能最差,密钥管理复杂 |
| 截断脱敏 | 只保留数据的前 N 位或后 N 位 | 非核心字段展示 | 简单高效 | 丢失数据完整性 |
| 洗牌脱敏 | 打乱数据内部字符顺序 | 地址、姓名 | 保留数据长度和字符集 | 可能保留部分语义信息 |
企业级分层脱敏架构🏗️
我们团队采用的是分层脱敏方案,在数据流转的各个环节都做防护,避免单点失效。
各层具体实现:
- 前端展示层:前端组件统一处理,只展示脱敏后的数据,原始数据不传到前端
- 接口层:使用 Spring AOP 拦截所有接口响应,对标注了@Sensitive注解的字段自动脱敏
- 日志层:重写 logback 的
MessageConverter,自动过滤日志中的敏感数据 - 数据访问层:基于 MyBatis 的
Interceptor实现查询结果的自动脱敏和插入前的加密 - 数据库层:生产库定期导出到测试库时,使用脚本批量静态脱敏
关键技术难点与解决方案⚠️
1.动态脱敏问题
- 问题:不同角色看到的脱敏程度不同(管理员看完整数据,普通员工看脱敏数据)
- 解决方案:基于 Spring Security 的权限体系,在 AOP 中根据当前用户角色动态选择脱敏策略
2.脱敏与业务解耦
- 问题:如果在业务代码中硬编码脱敏逻辑,会导致代码混乱且难以维护
- 解决方案:自定义
@Sensitive注解,配合 AOP 实现声明式脱敏,业务代码无感知
// 自定义脱敏注解
@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. 基础定义层(技术亮点:开闭原则设计)
/**
* 脱敏类型枚举
* 新增脱敏类型只需在这里添加,无需修改原有业务代码
*/
public enum SensitiveType {
PHONE, // 手机号
ID_CARD, // 身份证
BANK_CARD, // 银行卡
EMAIL, // 邮箱
NAME, // 姓名
ADDRESS, // 地址
CUSTOM // 自定义策略
}
/**
* 脱敏策略接口(策略模式核心)
* 所有脱敏算法都实现此接口,支持Spring自动注入
*/
public interface SensitiveStrategy {
String desensitize(String original);
}2. 默认脱敏策略实现
/**
* 手机号脱敏策略
* 保留前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. 自定义脱敏注解
/**
* 敏感字段注解
* 标注在实体类字段上,自动触发脱敏
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
SensitiveType value() default SensitiveType.CUSTOM;
// 支持自定义脱敏策略类(扩展点)
Class<? extends SensitiveStrategy> strategy() default SensitiveStrategy.class;
}4. Spring AOP 动态脱敏切面(核心技术亮点🔥)
/**
* 全局响应脱敏切面
* 技术亮点:
* 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 全局日志脱敏(容易忽略的安全点⚠️)
/**
* 日志脱敏转换器
* 技术亮点:全局统一处理,避免业务代码中手动过滤敏感数据
*/
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 实现
- 可扩展性强:新增脱敏算法只需实现一个接口
- 动态灵活:支持基于角色的细粒度脱敏
- 性能优秀:缓存策略实例,支持分布式批量处理
最重要的是,我们建立了定期的脱敏效果审计机制,避免出现 "假脱敏" 的情况。
真实面试模拟
真实面试模拟
👨💼 面试官:
同学你好,今天场景设计题我们聊个实际的——你在项目中数据脱敏是怎么做的? 不用面面俱到,把你认为最关键的几层设计讲清楚就行。
🧑💻 候选人:
好的面试官。我们主要是按展示、日志、存储、导出四层来分别处理的,先给您看个总览图,心里有个谱:
┌─────────────────────────────────────────────────────────┐
│ 数据脱敏体系 │
├─────────────┬──────────────┬──────────────┬─────────────┤
│ 展示层 │ 日志层 │ 传输/存储层 │ 数据导出层 │
│ (动态脱敏) │ (日志脱敏) │ (静态脱敏) │ (静态脱敏) │
├─────────────┼──────────────┼──────────────┼─────────────┤
│ 注解+AOP │ logback │ 数据库字段 │ EasyExcel │
│ Jackson序列化│ 自定义Converter│ 加密存储(AES) │ 脱敏注解 │
│ 网关脱敏 │ 基于正则替换 │ 脱敏视图 │ 异步脱敏处理 │
└─────────────┴──────────────┴──────────────┴─────────────┘👨💼 面试官:
图很清晰 👍 先说说展示层动态脱敏吧,你们是怎么让前端看到的接口 JSON 里手机号直接变成 138****5678 的?
🧑💻 候选人:
这块核心用的是 Jackson 自定义序列化 + 注解,对业务代码侵入特别小。
// ① 自定义脱敏注解
@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 字段上加个注解就行:
public class UserVO {
@Sensitive(type = SensitiveType.MOBILE)
private String mobile;
}💡 注意点:高并发下别在策略里临时编译正则,提前缓存 Pattern,压测 QPS 影响不超过 2%。
👨💼 面试官:
如果系统里字段特别多,总不能一个个手加注解吧?
🧑💻 候选人:
对,所以我们还会做“敏感数据自动发现”,用正则扫描元数据打标,然后通过配置中心动态驱动序列化,做到新接口上线零改动。
👨💼 面试官:
好,那日志脱敏你们怎么防止把身份证打到 ELK 里?
🧑💻 候选人:
全局重写了 logback 的 MessageConverter,匹配敏感信息替换,开发无感知。
public class SensitiveLogConverter extends MessageConverter {
@Override
public String convert(ILoggingEvent event) {
String msg = super.convert(event);
return SensitiveLogUtil.desensitize(msg); // 链式正则替换
}
}logback.xml 配置一行:
<conversionRule conversionWord="msg"
converterClass="com.xxx.SensitiveLogConverter" />🛡️ 还把 MDC 里的 userId 做了哈希处理,这样就算日志泄露也关联不到真人。
👨💼 面试官:
日志层过了,那数据库存储这层怎么办?有些场景还得能还原对吧。
🧑💻 候选人:
对,实名数据需要可逆,我们用应用层透明加解密,MyBatis 的 TypeHandler 搞定:
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 拦截器动态处理。
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. 展示层:可缓存策略的序列化器(解决性能)
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
@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. 敏感数据自动发现扫描器(解决字段打标难题)
@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 值;异常信息在全局异常处理器中二次脱敏 |
难点解决流程图(简化)
┌─────────────┐ ┌──────────────────┐ ┌───────────────┐
│ 数据请求到达 │────▶│ 网关层统一脱敏(可选) │────▶│ 业务服务 │
└─────────────┘ └──────────────────┘ └───────┬───────┘
│
┌───────────────────────────────┤
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Jackson序列化 │ │ 日志脱敏Converter │
│ (缓存策略+正则) │ │ (全局+MDC) │
└─────────────────┘ └──────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ 返回脱敏后JSON │ │ 输出安全日志 │
└─────────────────┘ └──────────────────┘👨💼 面试官:
很好,把细节和坑都讲清楚了。特别是性能缓存和自动发现这两个点,确实能看出实际落地的深度 👍 那今天数据脱敏这块我们就聊到这里。
🧑💻 候选人:
谢谢面试官 😄
