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

news2024/9/23 11:14:06

文章目录

  • 什么是数据脱敏
    • 静态数据脱敏
    • 动态数据脱敏
  • 需求
  • 实现
    • 1. 切面AOP实现脱敏
      • 是否脱敏注解
      • 定义切入点
      • 测试
        • 单条记录结果
        • 多条记录结果
        • 分页记录结果
    • 2. 自定义注解和自定义消息转换器实现数据脱敏
      • 自定义`DataDesensitization`注解
      • 定义脱敏类型枚举
      • 实现`AnnotationFormatterFactory`接口
      • 创建格式化类实现`Formatter`
      • 脱敏数据处理工具类`DataDesensitizationUtil`
      • 创建`ValueDesensitizeFilter`实现`ValueFilter`
      • 把`DataDesensitizationFormatterFactory`添加到`spring`
      • 测试
      • 拓展

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

什么是数据脱敏

数据脱敏(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 {
}
/**
 * @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);
        }

第三步:进行脱敏。

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

  1. 分页查询结果。(如baomidou格式响应)
  2. 多条查询结果。(如List格式响应)
  3. 单条查询结果。(如对象格式响应)
//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;
    }

测试

单条记录结果

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

多条记录结果

在这里插入图片描述

在这里插入图片描述

分页记录结果

在这里插入图片描述

在这里插入图片描述

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

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

自定义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);
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/855103.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SAP度量单位转换功能

针对今天N2项目提出业务痛点&#xff1a;物料30011110的基本单位是KG&#xff0c;在XXX的BOM里单位是G&#xff0c;由于物料没配单位转换关系&#xff0c;但系统又能正常进行转换&#xff0c;开发需要技术支持。 经专项调查&#xff0c;G和KG的转换是SAP相同量纲转换标准功能&…

econml双机器学习实现连续干预和预测

连续干预 在这个示例中&#xff0c;我们使用LinearDML模型&#xff0c;使用随机森林回归模型来估计因果效应。我们首先模拟数据&#xff0c;然后模型&#xff0c;并使用方法来effect创建不同干预值下的效应&#xff08;Conditional Average Treatment Effect&#xff0c;CATE&…

现在转行搞嵌入式找工作难不难啊?

对于应届生来说&#xff0c;嵌入式开发的经验不会有太多&#xff0c;所以要求也不会太高。 嵌入式开发常用的是C语言&#xff0c;所以需要你有扎实的功底&#xff0c;这一点很重要&#xff0c;数据结构算法&#xff0c;指针&#xff0c;函数&#xff0c;网络编程 有了上面的基…

微服务——数据同步

问题分析 mysql和redis之间有数据同步问题&#xff0c;ES和mysql之间也有数据同步问题。 单体项目可以在crud时就直接去修改&#xff0c;但在微服务里面不同的服务不行。 方案一 方案二 方案三 总结 导入酒店管理项目 倒入完成功启动后可以看见数据成功获取到了 声明队列和…

word横向页面侧面页码设置及转pdf后横线变竖线的解决方案

在处理材料的时候&#xff0c;会遇到同一个文档里自某一页开始&#xff0c;页面布局是横向的&#xff0c;这时候页码要设置在侧面&#xff0c;方法是双击页脚&#xff0c;然后在word工具栏上选择“插入”——>“文本框”——>“绘制竖版文本框”&#xff0c;然后在页面左…

Air001基于Arduino点灯实验

Air001基于Arduino点灯实验 &#x1f449;&#x1f3fb;Arduino平台环境搭建可以参考&#xff1a;https://wiki.luatos.com/chips/air001/Air001-Arduino.html &#x1f516;使用国内的json&#xff0c;安装不仅成功率高还非常的快速。&#xff08;当然取决于个人网络环境&…

Windows安装Redis

自己电脑做个测试&#xff0c;需要用到Redis&#xff0c;把安装过程记录下&#xff0c;方便有需要的人 1、找到下载地址&#xff1a;Releases microsoftarchive/redis GitHub Windows的Redis需要到GitHub上下载&#xff1a; 2、下载完后设置密码&#xff0c;打开文件夹&…

RemObjects Suite Subscription for Delphi Crack

RemObjects Suite Subscription for Delphi Crack Delphi的RemObjects套件订阅提供了一种方便且经济高效的方式&#xff0c;可以随时了解所有RemObjects Delphi产品的最新情况。它允许您访问所有当前和即将推出的产品&#xff0c;而不必担心错过重要的更新或新产品。当您的平台…

医院智慧运营管理数字化方案

近年来&#xff0c;随着医院的发展规模不断扩大&#xff0c;其人、财、物、技术等资源的配置活动愈加复杂&#xff0c;资产管理、成本管理、收支核算管理、资金预算管理、绩效管理等经济活动日常管理也日益繁重&#xff0c;医院对于数字化提升精细化管理的需求也愈发迫切。 国…

块、行内块水平垂直居中

1.定位实现水平垂直居中 <div class"outer"><div class"test inner1">定位实现水平垂直居中</div></div><style>.outer {width: 300px;height: 300px;border: 1px solid gray;margin: 100px auto 0;position: relative;}.te…

DeepSort算法简介

SORT算法 SORT(Simple Online and Realtime Tracking)算法是一种目标追踪算法&#xff0c;其简单有效&#xff0c;基于IOU来匹配&#xff0c;并且融入了卡尔曼滤波和匈牙利算法来降低ID Switch(可以说&#xff0c;追踪算法的目标只有两个&#xff1a;1.提高速度 2.降低ID Swit…

JMeter有对手了?RunnerGo这些功能真不错!

当谈到对于性能测试的需求时&#xff0c;JMeter和RunnerGo在测试场景设置、执行性能测试、性能测试结果分析等方面都提供了很多功能&#xff0c;但两个工具仍然存在一些区别。以下是详细的比较分析&#xff1a; 层次分明的模块化设计告别文件管理混乱&#xff1a; JMeter的设…

SpringBoot异步框架

参考&#xff1a;解剖SpringBoot异步线程池框架_哔哩哔哩_bilibili 1、 为什么要用异步框架&#xff0c;它解决什么问题&#xff1f; 在SpringBoot的日常开发中&#xff0c;一般都是同步调用的。但经常有特殊业务需要做异步来处理&#xff0c;例如&#xff1a;注册新用户&…

idea找不到DataBase

一、我想把数据库跟我的idea链接&#xff0c;结果发现找不到。如图。 二、解决方案 找到 file ---setting 找到plugin------找到marketplace 我的已经出现了

贪心-leetcode409最长回文串

贪心-leetcode409最长回文串 思考&#xff1a; 总的思路&#xff1a;从数字中选择个数是偶数的去union&#xff0c;最后如果有过奇数则多union一个作为中心&#xff1b;从数组中select然后feasible是如果该数字为偶数则union到两边&#xff0c;如果为奇数则按照偶数处理再给中…

Java版企业电子招标采购系统源码—企业战略布局下的采购寻源tbms

​ 项目说明 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&#xff0c;以…

分布式 - 消息队列Kafka:Kafka生产者发送消息的分区策略

文章目录 1. PartitionInfo 分区源码2. Partitioner 分区器接口源码3. 自定义分区策略4. 轮询策略 RoundRobinPartitioner5. 黏性分区策略 UniformStickyPartitioner6. hash分区策略7. 默认分区策略 DefaultPartitioner 分区的作用就是提供负载均衡的能力&#xff0c;或者说对数…

【Shell编程】Linux Shell编程入门:输入输出、变量、条件判断、函数和运算

在Linux操作系统中&#xff0c;Shell编程是一项非常重要的技能。通过Shell编程&#xff0c;我们可以自动化执行各种任务&#xff0c;提高工作效率。无论是系统管理、数据处理还是软件开发&#xff0c;都离不开Shell脚本的应用。本篇博客将带领大家深入了解Shell编程的基础知识&…

木马免杀(篇一)基础知识学习

木马免杀&#xff08;篇一&#xff09;基础知识学习 ———— 简单的木马就是一个 exe 文件&#xff0c;比如今年hw流传的一张图&#xff1a;某可疑 exe 文件正在加载。当然木马还可能伪造成各式各样的文件&#xff0c;dll动态链接库文件、lnk快捷方式文件等&#xff0c;也可能…

音视频基础:分辨率、码率、帧率之间关系

基础 人类视觉系统 分辨率 像素&#xff1a; 是指由图像的小方格组成的&#xff0c;这些小方块都有一个明确的位置和被分配的色彩数值&#xff0c;小方格颜色和位置就决定该图像所呈现出来的样子&#xff1b;可以将像素视为整个图像中不可分割的单位或者是元素&#xff1b;像素…