Spring Boot & AOP实现动态数据脱敏

时间:2025-01-23 06:57:50
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); } } }