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

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

什么是数据脱敏

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

}
  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
统计套利是一种利用不同市场或不同证券之间的价格差异进行交易的策略。在实践中,统计套利经常涉及到多个证券之间的关系,并且需要进行复杂的计算和统计分析。Python是一种强大的编程语言,可以用于实现这种策略。 以下是一个简单的使用Python实现统计套利的步骤: 1. 选择需要进行统计套利的证券。这些证券通常具有相关性,例如同一行业的股票、跨国公司的股票等。 2. 收集所选证券的历史价格数据。这可以通过从金融数据提供商如雅虎财经或谷歌财经获取数据,或者使用Python库(如pandas-datareader)来获取数据。 3. 计算每个证券的收益率。根据所选证券的历史价格数据,计算每个证券的收益率。这可以通过计算每个证券的价格变化率来实现。 4. 计算每对证券之间的协方差。使用pandas库中的corr()函数计算每对证券之间的协方差。这可以帮助确定证券之间的相关性。 5. 构建线性回归模型。使用StatsModels库中的OLS()函数构建线性回归模型。该模型可以帮助确定每个证券的权重。 6. 计算每个证券的标准化收益率。根据每个证券的收益率和其权重,计算每个证券的标准化收益率。 7. 计算套利指数。根据每个证券的标准化收益率,计算套利指数。套利指数表示所有选定证券的加权平均值。 8. 制定交易策略。根据套利指数和选定证券的价格变化,制定交易策略。 虽然以上步骤是一个简单的指南,但实际实现统计套利需要更多的计算和分析。但是,使用Python可以让这个过程更加高效和自动化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力的蚂蚁【你若】

如果帮助到了您,一分也是爱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值