Spring Boot & AOP实现动态数据脱敏
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);
}
}
}