Spring Boot AOP实现动态数据脱敏

news2025/1/23 8:58:02

依赖&配置


<!--  Spring Boot AOP起步依赖  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
/**
 * @Author: 说淑人
 * @Date: 2025/1/18 23:03
 * @Description: 切面配置
 */
@Configuration
// ---- 该注解用于开启AOP功能。
@EnableAspectJAutoProxy
public class AspectConfig {
}

数据脱敏


 注解&修饰

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author: 说淑人
 * @Date: 2023-11-24
 * @Description: 授权业务脱敏AO类
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface OauthBizMask {

    // ---- 该注解用来修饰在控制器方法上以标注该方法的返回数据需要数据脱敏,其核心作用是
    // 为AOP提供切入点。
    // ---- 注意!根据切入方式的不同,该注解并不是必须的,下文在切入代码中提供了无需当前
    // 注解的切入方式。但我们并不推荐那么做,因为那会导致所有的接口都必须经历数据脱敏过程,
    // 即使我们并不想执行该操作。

}
    /**
     * 获取宇宙
     *
     * @param customerId 客户ID
     * @return 结果BO(客户业务宇宙VO回应)
     */
    @OauthBizMask
    @ApiOperation("获取宇宙")
    @GetMapping(value = "get/universe")
    public ResultBox<CustomerBizUniverseResponse> getUniverse(@ApiParam(value = "客户ID", required = true) @RequestParam(value = "customerId") Long customerId) {
        return ResultBox.result(customerBizDispatcher.getUniverse(customerId));
    }

    /**
     * 查询宇宙集
     *
     * @param customerBizQueryRequest 客户业务查询VO请求
     * @return 结果BO(查询BO(客户业务宇宙VO回应集))
     */
    @OauthBizMask
    @ApiOperation("查询宇宙集")
    @GetMapping(value = "query/universes")
    public ResultBox<QueryBox<CustomerBizUniverseResponse>> queryUniverses(@Valid @ModelAttribute CustomerBizQueryRequest customerBizQueryRequest) {
        return ResultBox.result(customerBizDispatcher.queryUniverses(customerBizQueryRequest));
    }
import com.ssr.world.biz.manage.model.eo.oauth.OauthBizMaskRuleEnum;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author: 说淑人
 * @Date: 2023-11-24
 * @Description: 授权业务脱敏规则AO类
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface OauthBizMaskRule {

    // ---- 该注解只对字符串类型的字段有效!
    // ---- 该注解对嵌套超过5层的对象字段无效!

    /**
     * 权限 -- 在拥有指定权限的情况下可以避免数据脱敏,该功能可以视个人情况保留/删除。
     */
    String authority() default "";

    /**
     * 规则 -- 具体数据脱敏规则
     */
    OauthBizMaskRuleEnum rule();

}

    /**
     * 账号
     */
    @ApiModelProperty(value = "账号", required = true)
    // ---- 设置拥有“root/customer/nomask”权限的可以免数据脱敏,数据脱敏规则为账号。
    @OauthBizMaskRule(authority = "root/customer/nomask", rule = OauthBizMaskRuleEnum.ACCOUNT)
    private String account;
    /**
     * 手机号码
     */
    @ApiModelProperty(value = "手机号码")
    @OauthBizMaskRule(authority = "root/customer/nomask", rule = OauthBizMaskRuleEnum.PHONE_NUMBER)
    private String phoneNumber;
    /**
     * 名称
     */
    @ApiModelProperty(value = "名称")
    @OauthBizMaskRule(authority = "root/customer/nomask", rule = OauthBizMaskRuleEnum.NAME)
    private String name;

 枚举&工具

import com.ssr.world.tool.pedestal.util.string.StringUtil;

import java.util.function.Function;

/**
 * @Author: 说淑人
 * @Date: 2022/1/12 下午8:18
 * @Description: 授权业务脱敏规则EO类
 */
public enum OauthBizMaskRuleEnum {

    /**
     * 授权业务脱敏规则枚举集
     */
    ACCOUNT(s -> s.replaceAll("(\\S{5})\\S{10}(\\S*)", "$1**********$2")),
    PHONE_NUMBER(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
    NAME(s -> s.charAt(0) + StringUtil.repeat('*', s.length() - 1)),
    ;

    public final Function<String, String> masker;

    OauthBizMaskRuleEnum(Function<String, String> masker) {
        this.masker = masker;
    }

}

 切面

    在对数据对象的字段进行反射遍历时,我们还需要考虑父类对象&嵌套对象的字段遍历。由于对象嵌套的层级可能非常深且还可能有相互嵌套的情况,因此在遍历&迭代时必须要限制层级以避免长遍历&死循环,以及还要尽可能避免不必要的遍历,例如原生/框架的类,从而尽可能的提升性能。关于这些问题在下文的代码中都有提及且处理,请仔细查看代码注释。
    下述代码对列表结构也做了处理,基本上可以直接拿来用。

import com.ssr.world.biz.manage.client.oauth.OauthBizStaffClient;
import com.ssr.world.biz.manage.model.ao.oauth.OauthBizMaskRule;
import com.ssr.world.biz.manage.model.eo.oauth.OauthBizUserType;
import com.ssr.world.biz.manage.model.eo.oauth.OauthBizUserTypeEnum;
import com.ssr.world.biz.manage.model.vo.response.oauth.OauthBizStaffAuthorityResponse;
import com.ssr.world.biz.manage.tool.util.oauth.OauthBizUtil;
import com.ssr.world.tool.pedestal.model.bo.result.ResultBox;
import com.ssr.world.tool.pedestal.util.string.StringUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Objects;

/**
 * @Author: 说淑人
 * @Date: 2023-11-24
 * @Description: 授权业务脱敏AO类
 */
@Aspect
@Component
public class OauthBizMaskAspect {

    @Autowired
    private OauthBizStaffClient oauthBizStaffClient;

//    /**
//     * 切入点
//     */
//    @Pointcut("execution(* com.ssr.world..controller..*(..))")
//    public void pointcut() {
//        // ---- 以工程路径下所有控制器方法为切入点。这种方式比较简便,因为无需额外注解进
//        // 行修饰。但对性能的损耗很大,因为所有的控制器方法都会被切入。
//    }

    /**
     * 切入点
     */
    @Pointcut("@annotation(com.ssr.world.biz.manage.model.ao.oauth.OauthBizMask)")
    public void pointcut() {
        // ---- 以修饰了@OauthBizMask注解的方法为切入点。
    }


    /**
     * 环绕
     *
     * @param proceedingJoinPoint 行动参与点
     * @return 值
     * @throws Throwable 可抛出
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // ---- 获取方法的执行结果。
        Object object = proceedingJoinPoint.proceed();
        // ---- 判断是否需要对当前请求的返回数据进行脱敏操作,如果未携带令牌/用户为客户/用
        // 户为超级管理员,则直接返回而不执行的数据脱敏操作(该逻辑视个人情况保留/删除)。
        if (StringUtil.isBlank(OauthBizUtil.getAuthorization()) ||
                OauthBizUserTypeEnum.CUSTOMER.equals(OauthBizUtil.getUserType())
                || OauthBizUserType.SHUO_SHU_REN.equals(OauthBizUtil.getAccount())) {
            return object;
        }
        // ---- 将控制器方法的返回值强制转化为ResultBox对象以获取内部的封装数据。(该逻辑
        // 视个人情况保留/删除)。
        ResultBox<?> resultBox = (ResultBox<?>) object;
        Object data = resultBox.getData();
        // ---- 迭代数据对象包括父类在内的所有字段,判断其是否标注了@OauthBizMask注解,是
        // 则对内部数据进行脱敏。
        if (Objects.nonNull(data)) {
            recursiveField(1, data.getClass(), data, 
                // ---- 获取员工权限作为数据脱敏的执行依据。
                oauthBizStaffClient.getStaffAuthorityMapCache(OauthBizUtil.getAccount()));
        }
        return object;
    }

    /**
     * 迭代字段
     *
     * @param tier         层级
     * @param clazz        类对象
     * @param data         数据
     * @param authorityMap 权限映射
     * @throws IllegalAccessException 非法访问异常
     */
    private void recursiveField(int tier, Class<?> clazz, Object data, Map<String, OauthBizStaffAuthorityResponse> authorityMap) throws IllegalAccessException {
        // ---- 如果嵌套层级超过5级则直接返回。层级限制是为了避免深度嵌套导致的性能问题,
        // 以及相互嵌套导致的死循环问题。
        if (tier > 5) {
            return;
        }
        // ---- 判断数据对象是否是集(及子类)类型 ,是则迭代内部所有对象的所有字段。注意!
        // 迭代集中的对象不需要增加层级。
        if (data instanceof Collection) {
            for (Object collectionData : (Collection<?>) data) {
                if (Objects.nonNull(collectionData)) {
                    recursiveField(tier, collectionData.getClass(), collectionData, authorityMap);
                }
            }
            return;
        }
        // ---- 如果数据对象不是集(及子类)类型,判断其是否是自开发的类型,否则直接返回。
        // 该判断可以帮助我们免去对原生/框架类的字段迭代,因为我们只能对自开发的类字段修
        // 饰@OauthBizMaskRule注解,从而有效提升性能。
        // ---- 当然,在极少数情况下,我们可能使用除"集类"以外的某些原生/框架类对象来承载自
        // 开发类对象。这种情况下当前逻辑会导致数据无法脱敏,因此后续可能需要和"集类"一样
        // 对这些类进行特殊处理。
        Package pack = clazz.getPackage();
        if (Objects.isNull(pack) || !pack.getName().startsWith("个人工程路径前缀,例如com.xxx.xxx")) {
            return;
        }
        // ---- 迭代当前class对象的所有直属字段,即非父类字段。
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // ---- 判断当前字段值是否为null,是则直接略过。
            field.setAccessible(true);
            Object fieldData = field.get(data);
            if (Objects.isNull(fieldData)) {
                continue;
            }
            // ---- 判断当前字段是否是字符串类型,否则对该嵌套对象进行字段迭代,随后返回。
            if (!(fieldData instanceof String)) {
                recursiveField(tier + 1, fieldData.getClass(), fieldData, authorityMap);
                continue;
            }
            // ---- 判断字符串字段是否直接修饰了@OauthBizMaskRule注解,否则直接略过。
            OauthBizMaskRule oauthBizMaskRule = field.getDeclaredAnnotation(OauthBizMaskRule.class);
            if (Objects.isNull(oauthBizMaskRule)) {
                continue;
            }
            // ---- 如果字符串字段修饰了@OauthBizMaskRule注解,判断当前员工是否拥有指定权
            // 限且未曾过期,否则直接略过(该逻辑视个人情况保留/删除)。
            String authorityCode = oauthBizMaskRule.authority();
            OauthBizStaffAuthorityResponse authority;
            if (StringUtil.isNotBlank(authorityCode) &&
                    Objects.nonNull(authority = authorityMap.get(authorityCode)) &&
                    new Date().before(authority.getExpireDatetime())) {
                continue;
            }
            // ---- 进行数据脱敏操作。
            System.out.println("字段名:" + field.getName());
            System.out.println("字段值:" + fieldData);
            String value = (String) fieldData;
            field.set(data, oauthBizMaskRule.rule().masker.apply(value));
        }
        // ---- 获取父类,如果父类存在,继续迭代。注意!父类不属于嵌套。
        Class<?> parentClass = clazz.getSuperclass();
        if (Objects.nonNull(parentClass)) {
            recursiveField(tier, parentClass, data, authorityMap);
        }
    }

}

 效果

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

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

相关文章

JavaScript —— 变量与运算符

变量与常量 let&#xff1a;用来定义变量&#xff0c;可以只声明不定义&#xff1b; 例如&#xff1a; <script type"module">let a; // 只声明不定义let x 2, name "kitty"; // 定义若干个变量let d { // 定义一个对象&#xff0c;类似于p…

YOLO-cls训练及踩坑记录

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一、模型训练 二、测试 三、踩坑记录 1、推理时设置的imgsz不生效 方法一&#xff1a; 方法二&#xff1a; 2、Windows下torchvision版本问题导致报错 总结 前…

Android BitmapShader简洁实现马赛克,Kotlin(一)

Android BitmapShader简洁实现马赛克&#xff0c;Kotlin&#xff08;一&#xff09; 这一篇&#xff0c; Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现马赛克效果&#xff0c;Kotlin&#xff08;3&#xff09;-CSDN博客 基于PorterDuffXfermode实现马…

全球化趋势与中资企业出海背景

1. 全球化趋势与中资企业出海背景 1.1 全球经济格局变化 全球经济格局正经历深刻变革&#xff0c;新兴经济体崛起&#xff0c;全球产业链重塑&#xff0c;中资企业出海面临新机遇与挑战。据世界银行数据&#xff0c;新兴市场和发展中经济体在全球 GDP 中占比已超 40%&#xff…

无人机在城市执法监管中的应用:技术革新与监管挑战

随着科技的不断进步&#xff0c;无人机技术在城市管理中的应用越来越广泛。无人机以其灵活性、高效性和低成本的优势&#xff0c;正在逐渐成为城市执法监管的得力助手。本文将探讨无人机在城市执法监管中的应用现状、技术优势以及面临的挑战。 无人机技术在城市执法监管中的应用…

总结6..

背包问题的解决过程 在解决问题之前&#xff0c;为描述方便&#xff0c;首先定义一些变量&#xff1a;Vi表示第 i 个物品的价值&#xff0c;Wi表示第 i 个物品的体积&#xff0c;定义V(i,j)&#xff1a;当前背包容量 j&#xff0c;前 i 个物品最佳组合对应的价值&#xff0c;同…

【2024年 CSDN博客之星】我的2024年创作之旅:从C语言到人工智能,个人成长与突破的全景回顾

我的2024年创作之旅&#xff1a;从C语言到人工智能&#xff0c;个人成长与突破的全景回顾 引言 回望2024年&#xff0c;我不仅收获了技术上的成长&#xff0c;更收获了来自CSDN平台上无数粉丝、朋友以及网友们的支持与鼓励。在这条创作之路上&#xff0c;CSDN不仅是我展示技术成…

等变即插即用图像重建

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 摘要 即插即用算法为解决反问题成像问题提供了一个流行的框架&#xff0c;该框架依赖于通过降噪器隐式定义图像先验。这些算法可以利用强大的预训练降噪器来解决各种成像任务&#xff0c;从而避免了在每…

MLCC电容、铝电解电容寿命计算及影响分析

如何评价MLCC的寿命 MLCC的寿命受温度条件和施加的DC电压条件影响&#xff0c;可以用下列加速方程式来表示。 例如&#xff0c;85oC,16V条件的高温负荷测试是比65oC4V环境高2,374.16倍的加速测试&#xff0c;MTTF(测试样本数40pcs,可信度60%情况下)预计为103,562,200h&#…

集成学习算法

集成学习算法 一、集成学习介绍 二、随机森林算法 1、Bootstrap随机抽样 2、Bagging 算法 3、训练算法 4、代码实现 三、Boosting 算法 AdaBoost 算法 1、强分类器与弱分类器 2、训练算法 3、代码实现 一、集成学习介绍 集成学习是通过多个模型的组合形成一个精度更…

算法6(力扣148)-排序链表

1、问题 给你链表的头结点 head &#xff0c;请将其按 升序 排列并返回 排序后的链表 。 2、采用例子 输入&#xff1a;head [4,2,1,3] 输出&#xff1a;[1,2,3,4] 3、实现思路 将链表拆分成节点&#xff0c;存入数组使用sort排序&#xff0c;再用reduce重建链接 4、具…

DNA结合之Motif_1:CNN

1&#xff0c;首先可以识别在KO前后的motif——》由CNN模型做出识别&#xff0c;看看这个有没有什么灵感 2&#xff0c;ZNF143等都可以使用来识别 3&#xff0c;暂时只使用单个peak文件&#xff0c;后期可以使用ENCODE中所有的对应的TF的peak文件 1&#xff0c;文件解压之后…

【unity游戏开发之InputSystem——02】InputAction的使用介绍(基于unity6开发介绍)

文章目录 一、InputAction简介1、InputAction是什么&#xff1f;2、示例 二、InputAction参数相关1、点击齿轮1.1 Actions 动作&#xff08;1&#xff09;动作类型&#xff08;Action Type&#xff09;&#xff08;2&#xff09;初始状态检查&#xff08;Initial State Check&a…

机器学习 vs 深度学习

目录 一、机器学习 1、实现原理 2、实施方法 二、深度学习 1、与机器学习的联系与区别 2、神经网络的历史发展 3、神经网络的基本概念 一、机器学习 1、实现原理 训练&#xff08;归纳&#xff09;和预测&#xff08;演绎&#xff09; 归纳: 从具体案例中抽象一般规律…

Docker核心命令与Yocto项目的高效应用

随着软件开发逐渐向分布式和容器化方向演进&#xff0c;Docker 已成为主流的容器化技术之一。它通过标准化的环境配置、资源隔离和高效的部署流程&#xff0c;大幅提高了开发和构建效率。Yocto 项目作为嵌入式 Linux 系统构建工具&#xff0c;与 Docker 的结合进一步增强了开发…

Linux通过docker部署京东矩阵容器服务

获取激活码 将京东无线宝app升级到最新版,然后打开首页,点击号 选择添加容器矩阵,然后获取激活码 运行容器 read -p "请输入你的激活码: " ACTIVECODE;read -p "请输入宿主机的缓存路径: " src;docker rm -f cmatrix;docker run -d -it --name cmatrix …

vue视频流播放,支持多种视频格式,如rmvb、mkv

先将视频转码为ts ffmpeg -i C:\test\3.rmvb -codec: copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls C:\test\a\output.m3u8 后端配置接口 import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.spring…

【Solr分词器】

Solr分词器 一&#xff0c;什么是solr分词器&#xff1f; 前面已经提到过&#xff0c;Solr是一个高性能的全文检索服务&#xff0c;基于Apache Lucene的&#xff0c;Lucene是一个用Java编写的开源的信息检索库&#xff0c;为全文索引和搜索提供了基础功能。 在Solr中&#xf…

OS2.【Linux】基本命令入门(1)

目录 1.操作系统是什么? 2.好操作系统的衡量标准 3.操作系统的核心工作 4.在计算机上所有行为都会被转换为硬件行为 5.文件 6.简单介绍一些基本命令 1.clear 2.pwd 3.ls 1.ls -l 2.隐藏文件的创建 3.ls -al 4.ls -ld 5.ls -F(注意是大写) 4.cd 1.cd .. "…

LabVIEW处理复杂系统和数据处理

LabVIEW 是一个图形化编程平台&#xff0c;广泛应用于自动化控制、数据采集、信号处理、仪器控制等复杂系统的开发。它的图形化界面使得开发人员能够直观地设计系统和算法&#xff0c;尤其适合处理需要实时数据分析、高精度控制和复杂硬件集成的应用场景。LabVIEW 提供丰富的库…