Spring Boot AOP实现动态数据脱敏

devtools/2025/1/23 6:13:43/

依赖&配置


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

数据脱敏


 注解&修饰

java">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提供切入点。// ---- 注意!根据切入方式的不同,该注解并不是必须的,下文在切入代码中提供了无需当前// 注解的切入方式。但我们并不推荐那么做,因为那会导致所有的接口都必须经历数据脱敏过程,// 即使我们并不想执行该操作。}
java">    /*** 获取宇宙** @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));}
java">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();}
java">    /*** 账号*/@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;

 枚举&工具

java">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;}}

 切面

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

java">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 {@Autowiredprivate 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.ppmy.cn/devtools/152804.html

相关文章

第7章:Python TDD测试Franc对象乘法功能

写在前面 这本书是我们老板推荐过的&#xff0c;我在《价值心法》的推荐书单里也看到了它。用了一段时间 Cursor 软件后&#xff0c;我突然思考&#xff0c;对于测试开发工程师来说&#xff0c;什么才更有价值呢&#xff1f;如何让 AI 工具更好地辅助自己写代码&#xff0c;或许…

【开发日记】微信小程序getBackgroundAudioManager播放背景音乐提示播放失败

【问题】 小程序在手机上打开&#xff0c;播放在线音频的时候会提示播放失败&#xff0c;但打印异常提示的是src为null&#xff0c;自己在打印的时候却没问题。 并且在微信开发者工具中播放的时候也是正常的&#xff0c;只有手机上打开使用时提示异常。 【解决】 如果你的链…

如何使用 reduce() 方法对数组进行聚合计算?

数组聚合计算&#xff1a;如何使用 reduce() 方法进行数组遍历和聚合&#xff1f; 在 JavaScript 中&#xff0c;reduce() 方法是数组遍历和聚合计算中非常强大的工具。它通过遍历数组中的所有元素&#xff0c;将数组中的每个元素逐步汇总成一个单一的输出值&#xff08;如数值…

Spark任务提交流程

当包含在application master中的spark-driver启动后&#xff0c;会与资源调度平台交互获取其他执行器资源&#xff0c;并通过反向注册通知对应的node节点启动执行容器。此外&#xff0c;还会根据程序的执行规划生成两个非常重要的东西&#xff0c;一个是根据spark任务执行计划生…

python基础语句整理

Python是一种广泛使用的高级编程语言&#xff0c;以其简洁易读的语法而著称。以下是Python中的一些基础语句和概念&#xff0c;适合初学者了解&#xff1a; 1. 变量赋值 在Python中&#xff0c;变量用于存储数据值。变量名可以包含字母、数字和下划线&#xff0c;但不能以数字…

flutter入门系列教程<一>:tab组件的灵活妙用

文章目录 说明区分TabBarView组件TabBarViewTabBar实例 需求升级写在中间的tabbar组件封装组件组件说明组件用法示例 常规的tabbar封装常规用法 说明 前提&#xff1a;假设你已初步了解了flutter和dart语言&#xff0c;并且知道怎么创建一个简单的项目&#xff1b; 学习本文后…

代码随想录算法【Day29】

Day29 134. 加油站 暴力法 遍历每一个加油站为起点的情况&#xff0c;进行模拟 class Solution { public:int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {for(int i 0; i < cost.size(); i){ //以谁为起点int rest gas[i] - cos…

HDFS HADOOP分布式文件系统

目录 一、 HDFS概述 1.1 HDFS简介 1.2 HDFS优缺点 1.2.1 优点 1.2.2 缺点 1.3 HDFS组成架构 1.4 HDFS文件块大小 二、HDFS的Shell操作&#xff08;开发重点&#xff09; 2.1 基本语法 2.2 命令大全 2.3 常用命令实操 2.3.1 上传 2.3.2 下载 2.3.3 HDFS直接操作 三、HDFS的API操…