一、核心依赖引入与环境配置
本方案基于Java生态中最主流的MyBatis-Plus框架实现应用层动态脱敏,配合MySQL脚本处理存量数据。首先确保你的项目使用的是Spring Boot 2.x或3.x版本,并在pom.xml中引入必要的依赖。请直接复制以下坐标到你的pom文件中,不要遗漏版本号,以免引起兼容性冲突。
1. Maven依赖配置
在pom.xml的节点内添加以下内容:
```
com.baomidou
mybatis-plus-boot-starter
3.5.3.1
org.projectlombok
lombok
1.18.24
provided
```
添加依赖后,务必执行Maven刷新操作,确保jar包下载完整。接下来的步骤将直接基于这些依赖进行代码开发,无需安装额外的中间件或第三方服务,保持系统架构的轻量化。
二、自定义脱敏注解与工具类开发
为了实现灵活的脱敏控制,我们需要定义一个注解来标记实体类中需要脱敏的字段,并编写一个工具类处理具体的脱敏逻辑。这种方式侵入性最小,开发人员只需在实体字段上加注解即可。
1. 定义脱敏注解
创建名为DataMasking的注解类。该注解用于标记在实体类的字段上,指定脱敏类型。请完整复制以下代码:
```
package com.example.archive.annotation;
import java.lang.annotation.;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataMasking {
/
脱敏类型枚举
/
MaskingTypeEnum type();
}
```
同时创建对应的枚举类MaskingTypeEnum,定义数字档案馆常见的敏感数据类型:
```
package com.example.archive.annotation;
public enum MaskingTypeEnum {
// 身份证号:保留前3后4
ID_CARD,
// 手机号:保留前3后4
PHONE_NUMBER,
// 姓名:保留姓,名用代替
CHINESE_NAME,
// 电子邮箱:保留首字母和@后缀
EMAIL
}
```
2. 编写脱敏工具类
创建DataMaskingUtil类,实现具体的字符串截取和替换逻辑。这里必须处理空指针和字符串长度不足的情况,防止生产环境报错。
```
package com.example.archive.util;
import com.example.archive.annotation.MaskingTypeEnum;
public class DataMaskingUtil {
public static String masking(String data, MaskingTypeEnum type) {
if (data == null || data.isEmpty()) {
return data;
}
switch (type) {
case ID_CARD:
return idCardMasking(data);
case PHONE_NUMBER:
return phoneMasking(data);
case CHINESE_NAME:
return nameMasking(data);
case EMAIL:
return emailMasking(data);
default:
return data;
}
}
private static String idCardMasking(String idCard) {
int len = idCard.length();
if (len < 8) return "";
return idCard.substring(0, 3) + "" + idCard.substring(len - 4);
}
private static String phoneMasking(String phone) {
if (phone.length() != 11) return "";
return phone.substring(0, 3) + "" + phone.substring(7);
}
private static String nameMasking(String name) {
if (name.length() == 1) return "";
return name.substring(0, 1) + "".repeat(Math.min(name.length() - 1, 2));
}
private static String emailMasking(String email) {
int index = email.indexOf("@");
if (index <= 1) return email;
return email.charAt(0) + "" + email.substring(index);
}
}
```
三、基于MyBatis拦截器的动态脱敏实现
这是本方案的核心部分。通过实现MyBatis的Interceptor接口,拦截查询结果集,在数据返回给前端前进行实时替换。这种方式不需要修改数据库中的原始数据,保证了档案数据的完整性。
1. 创建结果集拦截器
创建ResultMaskingInterceptor类。该类会在SQL执行后、结果映射时触发。请确保签名注解正确指向ResultSetHandler。
```
package com.example.archive.interceptor;
import com.example.archive.annotation.DataMasking;
import com.example.archive.util.DataMaskingUtil;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Properties;
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultMaskingInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 执行原查询逻辑
Object result = invocation.proceed();
if (result instanceof List) {
List> list = (List>) result;
for (Object obj : list) {
maskObject(obj);
}
} else {
maskObject(result);
}
return result;
}
private void maskObject(Object obj) throws IllegalAccessException {
if (obj == null) return;
// 使用MyBatis元工具反射获取字段
MetaObject metaObject = SystemMetaObject.forObject(obj);
// 获取原始对象的所有字段(包括父类)
Class> clazz = obj.getClass();
while (clazz != null) {
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(DataMasking.class)) {
DataMasking annotation = field.getAnnotation(DataMasking.class);
String fieldName = field.getName();
Object value = metaObject.getValue(fieldName);
if (value instanceof String) {
String maskedValue = DataMaskingUtil.masking((String) value, annotation.type());
metaObject.setValue(fieldName, maskedValue);
}
}
}
clazz = clazz.getSuperclass();
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
```
2. 注册拦截器配置

在Spring Boot配置类中注册上述拦截器。如果项目已有MyBatis配置类,直接将addInterceptors方法内的代码加入即可。
```
package com.example.archive.config;
import com.example.archive.interceptor.ResultMaskingInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
import java.util.List;
@Configuration
public class MyBatisConfig {
@Autowired
private List
sqlSessionFactoryList;
@PostConstruct
public void addInterceptor() {
ResultMaskingInterceptor interceptor = new ResultMaskingInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}
```
四、存量档案数据SQL批量脱敏脚本
数字档案馆系统中往往已经存在大量历史数据。对于非开发人员访问(如报表导出、数据迁移)或需要物理脱敏的场景,需要直接在数据库层面执行更新。以下脚本针对MySQL数据库,涵盖了身份证、手机号和姓名的批量处理。
1. 备份与安全检查
在执行任何更新前,必须强制执行备份。以下命令创建一个结构相同的临时表用于备份:
```
CREATE TABLE archive_user_bak_20231027 LIKE archive_user;
INSERT INTO archive_user_bak_20231027 SELECT FROM archive_user;
```
2. 执行脱敏更新
使用MySQL的字符串操作函数进行原地更新。请注意,执行前务必确认is_masked字段是否存在,如果不存在,建议先添加该字段标记是否已脱敏,防止重复执行导致数据彻底损坏。
```
-- 添加脱敏标记列(如果不存在)
ALTER TABLE archive_user ADD COLUMN is_masked TINYINT(1) DEFAULT 0 COMMENT '是否已脱敏';
-- 批量脱敏身份证号 (保留前3后4)
UPDATE archive_user
SET id_card = CONCAT(LEFT(id_card, 3), '', RIGHT(id_card, 4)),
is_masked = 1
WHERE is_masked = 0 AND LENGTH(id_card) >= 8;
-- 批量脱敏手机号 (保留前3后4)
UPDATE archive_user
SET phone_number = CONCAT(LEFT(phone_number, 3), '', RIGHT(phone_number, 4)),
is_masked = 1
WHERE is_masked = 0 AND phone_number REGEXP '^[0-9]{11}$';
-- 批量脱敏姓名 (保留首字)
UPDATE archive_user
SET real_name = CONCAT(LEFT(real_name, 1), ''),
is_masked = 1
WHERE is_masked = 0 AND CHAR_LENGTH(real_name) > 1;
```
3. 处理JSON字段内的敏感数据
现代档案馆系统常使用JSON字段存储扩展信息。MySQL 5.7+支持JSON函数。假设ext_info字段中包含mobile键,更新脚本如下:
```
-- 更新JSON字段中的手机号
UPDATE archive_user
SET ext_info = JSON_SET(
ext_info,
'$.mobile',
CONCAT(LEFT(JSON_UNQUOTE(JSON_EXTRACT(ext_info, '$.mobile')), 3), '', RIGHT(JSON_UNQUOTE(JSON_EXTRACT(ext_info, '$.mobile')), 4))
)
WHERE is_masked = 0
AND JSON_CONTAINS_PATH(ext_info, 'one', '$.mobile');
```
五、功能验证与效果确认
部署完成后,必须通过日志和实际返回值进行双重验证,确保脱敏规则生效且不影响业务逻辑。
1. 实体类标记测试
在你的用户实体类ArchiveUser中,对敏感字段添加注解:
```
public class ArchiveUser {
private Long id;
@DataMasking(type = MaskingTypeEnum.CHINESE_NAME)
private String realName;
@DataMasking(type = MaskingTypeEnum.ID_CARD)
private String idCard;
@DataMasking(type = MaskingTypeEnum.PHONE_NUMBER)
private String phoneNumber;
// getter and setter...
}
```
2. 接口响应验证
启动项目,调用查询用户列表的接口。预期返回的JSON数据中,realName应显示为“张”,idCard显示为“1101234”,phoneNumber显示为“1385678”。
3. 数据库原始数据验证
执行SQL查询数据库:SELECT FROM archive_user WHERE id = 1;。如果配置的是应用层拦截器,这里查出的数据应当依然是明文,只有通过接口返回的才是密文。如果执行了第四步的SQL脚本,这里查出的就是密文。请根据你的实际安全需求选择保留明文(推荐)或物理脱敏。