赞
踩
本文主要讲解数据脱敏以及实现数据脱敏的两种实现方式。
数据脱敏(Data Masking
),顾名思义,是屏蔽敏感数据,对某些敏感信息(比如,身份证号、手机号、卡号、客户姓名、客户地址、邮箱地址、薪资等等 )通过脱敏规则进行数据的变形,实现隐私数据的可靠保护。业界常见的脱敏规则有,替换、重排、加密、截断、掩码,用户也可以根据期望的脱敏算法自定义脱敏规则。
良好的数据脱敏实施,需要遵循如下两个原则, 第一,尽可能地为脱敏后的应用,保留脱敏前的有意义信息; 第二,最大程度地防止黑客进行破解。
这里我画一张图来更清楚的理解什么是数据脱敏。
数据脱敏又分为静态数据脱敏(SDM
)和 动态数据脱敏(DDM
):
静态数据脱敏,是数据的“搬移并仿真替换”,是将数据抽取进行脱敏处理后,下发给下游环节,随意取用和读写的,脱敏后数据与生产环境相隔离,满足业务需求的同时保障生产数据库的安全。
动态数据脱敏,在访问敏感数据的同时实时进行脱敏处理,可以为不同角色、不同权限、不同数据类型执行不同的脱敏方案,从而确保返回的数据可用而安全。(本文的实现方式就是动态数据脱敏)
如用户表数据,要求根据不同的角色查询时对返回数据进行脱敏处理。
管理员账号则返回原数据;
普通账号查询,返回带星号的数据。
该实现方式对于前端来说是调用一个接口,后端自动识别脱敏处理。
思路:使用AOP来进行处理结果,反射修改返回数据。
定义两个注解,一个在接口上使用,包含则表示该接口为需要脱敏接口。一个定义在实体类中,确定脱敏规则,是手机号还是身份证号等。
Aop切面统一处理结果,什么情况下进行脱敏。
1.包含脱敏注解
2.业务判断是否需要脱敏
3.根据实体字段注解类型来进行不同的脱敏
/**
* @author SunChangSheng
* @apiNote 数据脱敏注解,方法含有该注解则表示需要数据脱敏
* @since 2023/7/27 15:32
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataDesensitization {
}
/**
* @author SunChangSheng
* @apiNote
* @since 2023/7/27 16:00
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataDesensitizationType {
/**
* 类型:1手机号,2邮箱
* @return
*/
int type() default 1;
}
@Aspect
@Component
public class DataDesensitizationAspect {
private static final Logger log = LoggerFactory.getLogger(DataDesensitizationAspect.class);
//定义了一个切入点
@Pointcut("@annotation(com.ruoyi.common.data.DataDesensitization)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
return getResult(point);
}
private Object getResult(ProceedingJoinPoint point) throws Throwable {
return point.proceed();
}
}
在around
方法中进行补充逻辑。
第一步:需要判断该接口是否有脱敏注解,没有则直接返回。
//1.判断该方法是否包含脱敏注解
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
log.info("目标方法地址:{}", method.getName());
if (!method.isAnnotationPresent(DataDesensitization.class)) {
return getResult(point);
}
第二步:这步为可选逻辑,如我们只有指定的用户查看需要进行脱敏。
//2.业务判断是否需要脱敏 这里假设当用户id为3则进行脱敏。
boolean flag = SecurityUtils.getUserId() == 3;
if (!flag) {
return getResult(point);
}
第三步:进行脱敏。
这里我把响应结果大致归为三类,当然,可以自行补充实现。
baomidou
格式响应)List
格式响应)//3.根据脱敏规则进行脱敏
ApiResponse result = (ApiResponse) getResult(point);
return assertResult(result);
private static final String pageResultType = "com.baomidou.mybatisplus.extension.plugins.pagination.Page";
private static final String listResultType = "java.util.ArrayList";
private Object assertResult(ApiResponse result) throws Exception {
Object data = result.getData();
String className = data.getClass().getName();
switch (className) {
case pageResultType: {
assertPageOrListResult(data, 1);
break;
}
case listResultType: {
assertPageOrListResult(data, 2);
break;
}
default: {
assertOneResult(data, className);
break;
}
}
return result;
}
这里的组装方法assert
则需要用到反射来获取对象的字段和值,当字段包含DataDesensitizationType
注解,则根据type
参数来进行不同的脱敏规则处理。这里我只列出手机号加*。
/**
* 分页或列表结果组装
* @param data 数据
* @param type 类型:1分页,2列表
* @throws Exception
*/
private void assertPageOrListResult(Object data, Integer type) throws Exception {
List list = new ArrayList();
if (type == 1) {
Page page = (Page) data;
list = page.getRecords();
} else {
list = (List) data;
}
for (Object record : list) {
Class<?> targetClass = Class.forName(record.getClass().getName());
Field[] fields = targetClass.getDeclaredFields();
reflexUpdateData(record, fields);
}
}
/**
* 单挑结果组装
* @param data 数据
* @param className 类名
* @throws Exception
*/
private void assertOneResult(Object data, String className) throws Exception {
//当条数据详情
Class<?> targetClass = Class.forName(className);
Field[] fields = targetClass.getDeclaredFields();
reflexUpdateData(data, fields);
}
private void reflexUpdateData(Object data, Field[] fields) throws IllegalAccessException {
for (Field field : fields) {
// 设置字段可访问, 否则无法访问private修饰的变量值
field.setAccessible(true);
Object value = field.get(data);
Annotation[] annotations = field.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof DataDesensitizationType) {
DataDesensitizationType targetAnnotation = (DataDesensitizationType) annotation;
int type = targetAnnotation.type();
// 获取字段名称
if (type == 1) {
field.set(data, desensitization1(value + ""));
}
}
}
}
}
private String desensitization1(String phone) {
String res = "";
if (!StringUtils.isEmpty(phone)) {
StringBuilder stringBuilder = new StringBuilder(phone);
res = stringBuilder.replace(3, 7, "****").toString();
}
return res;
}
该实现方式对于前端来说是调用一个接口,后端自动识别脱敏处理。
DataDesensitization
注解@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface DataDesensitization {
//脱敏类型
DataDesensitizationTypeEnum type();
}
public enum DataDesensitizationTypeEnum {
PHONE,
EMAIL,
ID_CARD;
}
AnnotationFormatterFactory
接口public class DataDesensitizationFormatterFactory implements AnnotationFormatterFactory<DataDesensitization> {
@Override
public Set<Class<?>> getFieldTypes() {
Set<Class<?>> hashSet = new HashSet<>();
hashSet.add(String.class);
return hashSet;
}
@Override
public Printer<?> getPrinter(DataDesensitization dataDesensitization, Class<?> aClass) {
return getFormatter(dataDesensitization);
}
@Override
public Parser<?> getParser(DataDesensitization dataDesensitization, Class<?> aClass) {
return getFormatter(dataDesensitization);
}
private DataDesensitizationFormatter getFormatter(DataDesensitization desensitization) {
DataDesensitizationFormatter formatter = new DataDesensitizationFormatter();
formatter.setTypeEnum(desensitization.type());
return formatter;
}
}
Formatter
public class DataDesensitizationFormatter implements Formatter<String> {
private DataDesensitizationTypeEnum typeEnum;
public DataDesensitizationTypeEnum getTypeEnum() {
return typeEnum;
}
public void setTypeEnum(DataDesensitizationTypeEnum typeEnum) {
this.typeEnum = typeEnum;
}
@Override
public String parse(String value, Locale locale) {
if (StringUtils.isNotBlank(value)) {
switch (typeEnum) {
case PHONE:
value = DataDesensitizationUtil.handlePhone(value);
break;
case EMAIL:
value = DataDesensitizationUtil.handleEmail(value);
break;
case ID_CARD:
value = DataDesensitizationUtil.handleIdCard(value);
break;
default:
}
}
return value;
}
@Override
public String print(String s, Locale locale) {
return s;
}
}
DataDesensitizationUtil
/**
* @author SunChangSheng
* @apiNote 脱敏数据处理
* @since 2023/7/31 21:04
*/
public class DataDesensitizationUtil {
public static String handlePhone(String value) {
if (StringUtils.isBlank(value)) {
return "";
}
return StringUtils.left(value, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(value, 4), StringUtils.length(value)
, "*"), "***"));
}
public static String handleEmail(String email) {
if (StringUtils.isBlank(email)) {
return "";
}
int index = StringUtils.indexOf(email, "@");
if (index <= 1) {
return email;
} else {
return StringUtils.rightPad(StringUtils.left(email, 3), index, "*").concat(StringUtils.mid(email, index, StringUtils.length(email)));
}
}
public static String handleIdCard(String value) {
if (StringUtils.isBlank(value)) {
return "";
}
return StringUtils.left(value, 6).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(value, 4), StringUtils.length(value)
, "*"), "******"));
}
}
ValueDesensitizeFilter
实现ValueFilter
/**
* @author SunChangSheng
* @apiNote fastjson的值过滤器ValueFilter
* @since 2023/7/31 21:07
*/
public class ValueDesensitizeFilter implements ValueFilter {
@Override
public Object process(Object object, String name, Object value) {
if (null == value || !(value instanceof String) || ((String) value).length() == 0) {
return value;
}
try {
Field field = object.getClass().getDeclaredField(name);
DataDesensitization desensitization;
if (String.class != field.getType() || (desensitization = field.getAnnotation(DataDesensitization.class)) == null) {
return value;
}
String valueStr = (String) value;
DataDesensitizationTypeEnum type = desensitization.type();
switch (type) {
case PHONE:
return DataDesensitizationUtil.handlePhone(valueStr);
case EMAIL:
return DataDesensitizationUtil.handleEmail(valueStr);
case ID_CARD:
return DataDesensitizationUtil.handleIdCard(valueStr);
default:
}
} catch (NoSuchFieldException e) {
return value;
}
return value;
}
}
DataDesensitizationFormatterFactory
添加到spring
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatterForFieldAnnotation(new DataDesensitizationFormatterFactory());
}
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
// 1.定义一个converters转换消息的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
// 2.添加fastjson的配置信息,比如: 是否需要格式化返回的json数据
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
//添加自己写的拦截器
fastJsonConfig.setSerializeFilters(new ValueDesensitizeFilter());
// 3.在converter中添加配置信息
fastConverter.setFastJsonConfig(fastJsonConfig);
// 4.将converter赋值给HttpMessageConverter
HttpMessageConverter<?> converter = fastConverter;
// 5.返回HttpMessageConverters对象
return new HttpMessageConverters(converter);
}
}
在实体类中加入注解:
@DataDesensitization(type = DataDesensitizationTypeEnum.PHONE)
如图:
这时直接调用用户详情接口、用户列表和分页查询接口。
上述代码是对于只要含有该实体类的方法就会进行脱敏,因为该实体类中包含了脱敏注解。
如果我们需要对不同的人来进行不同的处理,我们可以在过滤器增加逻辑。
也可以增加接口,定义VO,VO中加入脱敏注解,先查询出原数据,再进行目标实体类转换。不过这样对于前端来说增加了接口,这里只是说可以实现脱敏,也可以进行其他方式的拓展。
附上BaseHolder
类
@Component
public class BaseHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
BaseHolder.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return BaseHolder.applicationContext;
}
public static <T> T getBean(String beanName) {
return (T) BaseHolder.applicationContext.getBean(beanName);
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。