当前位置:   article > 正文

手把手教你 使用SpringBoot 实现业务数据动态脱敏_spingboot动态全局数据脱敏

spingboot动态全局数据脱敏

本文主要讲解数据脱敏以及实现数据脱敏的两种实现方式。

什么是数据脱敏

数据脱敏(Data Masking),顾名思义,是屏蔽敏感数据,对某些敏感信息(比如,身份证号、手机号、卡号、客户姓名、客户地址、邮箱地址、薪资等等 )通过脱敏规则进行数据的变形,实现隐私数据的可靠保护。业界常见的脱敏规则有,替换、重排、加密、截断、掩码,用户也可以根据期望的脱敏算法自定义脱敏规则。

良好的数据脱敏实施,需要遵循如下两个原则, 第一,尽可能地为脱敏后的应用,保留脱敏前的有意义信息; 第二,最大程度地防止黑客进行破解。

这里我画一张图来更清楚的理解什么是数据脱敏。

在这里插入图片描述

数据脱敏又分为静态数据脱敏(SDM)和 动态数据脱敏(DDM):

静态数据脱敏

静态数据脱敏,是数据的“搬移并仿真替换”,是将数据抽取进行脱敏处理后,下发给下游环节,随意取用和读写的,脱敏后数据与生产环境相隔离,满足业务需求的同时保障生产数据库的安全。

动态数据脱敏

动态数据脱敏,在访问敏感数据的同时实时进行脱敏处理,可以为不同角色、不同权限、不同数据类型执行不同的脱敏方案,从而确保返回的数据可用而安全。(本文的实现方式就是动态数据脱敏)

需求

如用户表数据,要求根据不同的角色查询时对返回数据进行脱敏处理。

管理员账号则返回原数据;

普通账号查询,返回带星号的数据。

实现

1. 切面AOP实现脱敏

该实现方式对于前端来说是调用一个接口,后端自动识别脱敏处理。

思路:使用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 {
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
/**
 * @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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

定义切入点

@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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

around方法中进行补充逻辑。

第一步:需要判断该接口是否有脱敏注解,没有则直接返回。

//1.判断该方法是否包含脱敏注解
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        log.info("目标方法地址:{}", method.getName());
        if (!method.isAnnotationPresent(DataDesensitization.class)) {
            return getResult(point);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

第二步:这步为可选逻辑,如我们只有指定的用户查看需要进行脱敏。

//2.业务判断是否需要脱敏 这里假设当用户id为3则进行脱敏。
        boolean flag = SecurityUtils.getUserId() == 3;
        if (!flag) {
            return getResult(point);
        }
  • 1
  • 2
  • 3
  • 4
  • 5

第三步:进行脱敏。

这里我把响应结果大致归为三类,当然,可以自行补充实现。

  1. 分页查询结果。(如baomidou格式响应)
  2. 多条查询结果。(如List格式响应)
  3. 单条查询结果。(如对象格式响应)
//3.根据脱敏规则进行脱敏
        ApiResponse result = (ApiResponse) getResult(point);
        return assertResult(result);
  • 1
  • 2
  • 3
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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这里的组装方法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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

测试

单条记录结果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

多条记录结果

在这里插入图片描述

在这里插入图片描述

分页记录结果

在这里插入图片描述

在这里插入图片描述

2. 自定义注解和自定义消息转换器实现数据脱敏

该实现方式对于前端来说是调用一个接口,后端自动识别脱敏处理。

自定义DataDesensitization注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface DataDesensitization {
    //脱敏类型
    DataDesensitizationTypeEnum type();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

定义脱敏类型枚举

public enum DataDesensitizationTypeEnum {
    PHONE,
    EMAIL,
    ID_CARD;
}
  • 1
  • 2
  • 3
  • 4
  • 5

实现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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

创建格式化类实现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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

脱敏数据处理工具类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)
                , "*"), "******"));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

创建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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

测试

在实体类中加入注解:

@DataDesensitization(type = DataDesensitizationTypeEnum.PHONE)
  • 1

如图:

在这里插入图片描述

这时直接调用用户详情接口、用户列表和分页查询接口。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

拓展

上述代码是对于只要含有该实体类的方法就会进行脱敏,因为该实体类中包含了脱敏注解。
如果我们需要对不同的人来进行不同的处理,我们可以在过滤器增加逻辑。

在这里插入图片描述

也可以增加接口,定义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);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/920869
推荐阅读
相关标签
  

闽ICP备14008679号