背景介绍
笔者的项目中有涉及的相关的出参数据需要脱敏,例如批量的列表展示的时候需要将身份信息,手机号码,邮箱等敏感信息进行脱敏展示,目的是为了避免批量泄露,但是在单条数据展示的时候又需要将这些数据展示出来,方便使用。为此笔者也是使用了AOP切面进行处理
架构思路
笔者考虑的是业务操作正常返回数据,只在控制层进行脱敏处理,这样数据使用方就只能使用脱敏后的数据,后端在各个组件之间业务数据交互上也没有影响。
代码实现
这次使用的切面表达式使用的是注解的方式
java
package com.cjt.demo.springaopdemo.annotation;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import java.lang.annotation.*;
/*************************
* @Project spring-aop-demo
* @PackageName com.cjt.demo.springaopdemo.annotation
* @DateTime 2024/7/2 0002 15:04
* @Author Cao Jiangtao
* @Describe 对方法进行拦截的注解
*************************/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
@ConditionalOnWebApplication
public @interface DesensitiseMethod {
}
java
package com.cjt.demo.springaopdemo.annotation;
import com.cjt.demo.springaopdemo.enums.FieldTypeEnums;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import java.lang.annotation.*;
/*************************
* @Project spring-aop-demo
* @PackageName com.cjt.demo.springaopdemo.annotation
* @DateTime 2024/6/26 0026 15:55
* @Author Cao Jiangtao
* @Describe 字段脱敏注解
*************************/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
@ConditionalOnWebApplication
public @interface DesensitiseField {
FieldTypeEnums value() default FieldTypeEnums.NULL;
}
上述两个注解,一个作用于类和方法(ElementType.METHOD, ElementType.TYPE),一个作用于实体属性(ElementType.FIELD),具体的注解使用方式见下图

主要使用的是@AfterReturning通知实现,在业务正常返回结果之后,重写结果进行页面展示
java
package com.cjt.demo.springaopdemo.aop;
import com.cjt.demo.springaopdemo.annotation.DesensitiseField;
import com.cjt.demo.springaopdemo.annotation.DesensitiseMethod;
import com.cjt.demo.springaopdemo.entity.Employee;
import com.cjt.demo.springaopdemo.enums.FieldTypeEnums;
import com.cjt.demo.springaopdemo.utils.MaskUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.description.annotation.AnnotationDescription;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Collectors;
/*************************
* @Project spring-aop-demo
* @PackageName com.cjt.demo.springaopdemo.aop
* @DateTime 2024/6/26 0026 15:48
* @Author Cao Jiangtao
* @Describe 脱敏处理注解类
*************************/
@Component
@Aspect
@Slf4j
public class DesensitiseAspect {
@Pointcut(value = "@annotation(com.cjt.demo.springaopdemo.annotation.DesensitiseMethod)")
public void cut() {
}
@AfterReturning(value = "cut()", returning = "result")
public void DesensitiseResponse(JoinPoint joinPoint, Object result) throws IllegalAccessException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
/***********************************************************************************
* 1. 如果返回的是一个集合,则需要看集合中的元素是否包含需要脱敏的字段,如有则需要逐个脱敏
* 2. 如果返回的直接是一个对象,则判断是否包含有需要脱敏的字段,如有则直接脱敏处理
***********************************************************************************/
if (method.getReturnType().getTypeName().equals("com.github.pagehelper.PageInfo")) { // 确认是分页返回,需要逐个处理
// 获取集合中数据,并确认集合中的数据是否有需要脱敏的注解
PageInfo pageInfo = (PageInfo) result;
List pageInfoList = pageInfo.getList();
List targetList = new ArrayList(pageInfoList.size());
Annotation[] annotations = method.getDeclaredAnnotations();
boolean present = Arrays.stream(annotations).
anyMatch(x -> x.annotationType().getName().equals("com.cjt.demo.springaopdemo.annotation.DesensitiseMethod"));
// 如果集合中没有需要脱敏的字段,直接跳出
if (present) {
// 返回的集合中按照实体类需脱敏字段逐个遍历处理
for (Object pageItem : pageInfoList) {
Class<?> itemClass = pageItem.getClass();
// 获取单个元素的所有字段属性
Field[] fields = itemClass.getDeclaredFields();
// 对象转换为map进行处理
ObjectMapper objectMapper = new ObjectMapper();
Map itemMap = objectMapper.convertValue(pageItem, Map.class);
for (Field field : fields) {
if (field.isAnnotationPresent(DesensitiseField.class)) {
field.setAccessible(true);
Object obj = itemMap.get(field.getName()); // 获取需要脱敏的值,如身份证,姓名等
FieldTypeEnums desensitiseType = field.getAnnotation(DesensitiseField.class).value();
if (obj instanceof String) {
String desVal = desensitise(String.valueOf(obj), desensitiseType);
itemMap.replace(field.getName(), desVal); // 重写脱敏后的值
}
}
}
targetList.add(objectMapper.convertValue(itemMap, itemClass)); // 当前的一条记录转换后添加到新集合中
}
pageInfo.setList(targetList); // 设置新的集合
}
} else {
Annotation[] annotations = method.getDeclaredAnnotations();
// 返回的实体类是否存在需要脱敏的注解类,如果存在就继续进行后续处理逻辑
boolean present = Arrays.stream(annotations).
anyMatch(x -> x.annotationType().getName().equals("com.cjt.demo.springaopdemo.annotation.DesensitiseMethod"));
if (present) {
// 标记有脱敏的方法才进行脱敏处理
// 利用反射获取当前实体中需要脱敏的字段
Field[] declaredFields = method.getReturnType().getDeclaredFields();
// 遍历实体类集合,对标注脱敏的字段进行脱敏处理
for (Field field : declaredFields) {
if (field.isAnnotationPresent(DesensitiseField.class)) {
field.setAccessible(true);
Object obj = field.get(result); // 获取需要脱敏的值,如身份证,姓名等
FieldTypeEnums desensitiseType = field.getAnnotation(DesensitiseField.class).value();
if (obj instanceof String) {
String desVal = desensitise(String.valueOf(obj), desensitiseType);
field.set(result, desVal); // 重写脱敏后的值
}
}
}
}
}
}
/**
* 根据类型对传入的对象进行脱敏处理
*
* @param val 待脱敏的字符串
* @param type 字符串对应的类型,按照类型进行脱敏
* @return 返回脱敏后的字符串
*/
private String desensitise(String val, FieldTypeEnums type) {
switch (type) {
case ID_NUMBER:
return MaskUtils.maskIdCardNo(val); // 按照身份证进行脱敏处理
case EMAIL:
return MaskUtils.maskEmail(val); // 按照邮箱进行脱敏处理
case FIXED_PHONE:
case MOBILE_PHONE:
return MaskUtils.maskMobile(val); // 按照电话进行脱敏处理
case ZH_NAME:
return MaskUtils.maskName(val); // 按照姓名进行脱敏处理
default:
return val;
}
}
}
演示效果


源码地址
笔者的demo代码使用的是 springboot单体架构
主要技术点: SpringBoot + Sqlite + knife4j + Mybatis
Jiangtao/spring-aop-demo
https://gitee.com/caojiangtao1989/spring-aop-demo.git
欢迎关注和指正