前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Rpamis-security-原理解析

Rpamis-security-原理解析

作者头像
benym
发布于 2023-12-14 06:16:26
发布于 2023-12-14 06:16:26
25000
代码可运行
举报
文章被收录于专栏:后端知识体系后端知识体系
运行总次数:0
代码可运行

# 核心组件

rpamis-security (opens new window)1.0.1主要通过Mybatis-PluginAOP切面实现安全功能,其主要组件如下图所示

# Mybatis插件前置知识

Mybatis中预留有org.apache.ibatis.plugin.Interceptor接口,通过实现该接口,开发者能够对Mybatis的执行流程进行拦截

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;

    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    default void setProperties(Properties properties) {
    }
}

三个方法的解释通常为

  • 【intercept】:插件执行的具体流程,传入的InvocationMyBatis对被代理的方法的封装。
  • 【plugin】:使用当前的Interceptor创建代理,通常的实现都是Plugin.wrap(target, this)wrap方法内使用jdk创建动态代理对象。
  • 【setProperties】:参考下方代码,在MyBatis配置文件中配置插件时可以设置参数,在setProperties函数中调用Properties.getProperty("param1")方法可以得到配置的值。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<plugins>
    <plugin interceptor="com.xx.xx.xxxInterceptor">
        <property name="param1" value="value1"/>
    </plugin>
</plugins>

# 插件原理

项目在启动时,Mybatisorg.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration方法内的pluginElement方法会解析配置文件中的plugins节点

之后configurationaddInterceptor方法会将拦截器加入到拦截器链中

在执行SQL时,所有的插件都会依次执行

对于一个Mybatis的操作而言,其能够被代理的几个概念为

  • 【Executor】: 真正执行SQL语句的对象,调用sqlSession的方法时,本质上都是调用executor的方法,还负责获取connection,创建StatementHandler
  • 【StatementHandler】: 创建并持有ParameterHandlerResultSetHandler对象,操作JDBCstatement与进行数据库操作。
  • 【ParameterHandler】: 处理入参,将Java方法上的参数设置到被执行语句中。
  • 【ResultSetHandler】: 处理SQL语句的执行结果,将返回值转换为Java对象。

执行顺序由前到后

可以配合对应注解实现对不同阶段不同执行方法的拦截

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
        @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })

比如这里是在Executor阶段对updatequery方法进行拦截

# 普通SQL加密插件-MybatisEncryptInterceptor

对于一个基本的字段加密功能,可如网络中常见的教程一样,拦截ParameterHandler阶段的setParameters方法,因为此时正是处理Java参数和执行语句的时间

本项目也采用了这种处理方式

  1. 首先获取真正需要处理的对象,并从中获取对应的参数
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (!(invocation.getTarget() instanceof ParameterHandler)) {
    return invocation.proceed();
}
ParameterHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 如果是select操作,或者mybatis-plus的@SqlParser(filter = true)跳过该方法解析,不进行验证
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("mappedStatement");
if (SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
    return invocation.proceed();
}
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
Object parameterObject = parameterHandler.getParameterObject();
if (Objects.isNull(parameterObject)) {
    return invocation.proceed();
}
  1. 对于返回值是ListMap类型且包含Mybatis-Plus实体key的进行通用加密处理
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// mybatis对于List类型的处理,底层默认为Map类型
if (parameterObject instanceof Map) {
    Map<String, Object> parameterObjectMap = (Map<String, Object>) parameterObject;
    // 如果不包含mybatis-plus的实体key
    if (parameterObjectMap.containsKey(Constants.ENTITY)) {
        Object entity = parameterObjectMap.get(Constants.ENTITY);
        if (entity != null) {
            // 深拷贝复制
            Object deepCloneEntity = SerializationUtils.deepClone(entity);
            if (Objects.nonNull(deepCloneEntity)) {
                // 进行加密
                Object encryptObject = securityResolver.encryptFiled(deepCloneEntity);
                parameterObjectMap.put(Constants.ENTITY, encryptObject);
            }
        }
    }
}

通常而言SQL的返回值可分为ListMapDO实体Page对象等,其中Mybatis会将List最终转化为Map进行处理。

  1. 对于返回值是非ListMap的类型,获取ParameterHandler中的parameterObject字段,进行通用加密处理
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
else {
    // mybatis处理
    Object deepCloneEntity = SerializationUtils.deepClone(parameterObject);
    if (Objects.nonNull(deepCloneEntity)) {
        Field field = parameterHandler.getClass().getDeclaredField("parameterObject");
        field.setAccessible(true);
        field.set(parameterHandler, deepCloneEntity);
        // 进行加密
        Object encryptObject = securityResolver.encryptFiled(deepCloneEntity);
    }
}

通用加密处理过程如图所示

对于任意需要解析的实体,我们需要寻找实体内所有被@SecurityField注解标记的字段

通常这个过程是自底向上的,即已知实体,搜索实体内所有的字段Filed,并过滤出被标记的字段

在项目中具体的实现过程为

com.rpamis.security.starter.utils.FieldUtils#getAllFields

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 获取包括父类所有的属性
 *
 * @param sourceObject 源对象
 * @return Field[]
 */
public static Field[] getAllFields(Object sourceObject) {
    // 获得当前类的所有属性(private、protected、public)
    List<Field> fieldList = new ArrayList<>();
    Class<?> tempClass = sourceObject.getClass();
    String objString = "java.lang.object";
    // 当父类为null的时候说明到达了最上层的父类(Object类).
    while (tempClass != null && !tempClass.getName().toLowerCase().equals(objString)) {
        fieldList.addAll(Arrays.asList(tempClass.getDeclaredFields()));
        // 得到父类,然后赋给自己
        tempClass = tempClass.getSuperclass();
    }
    Field[] fields = new Field[fieldList.size()];
    fieldList.toArray(fields);
    return fields;
}

com.rpamis.security.starter.utils.SecurityResolver#getParamsFields

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 获取原始实体内所有被SecurityField标记的Field
 *
 * @param params SecurityField
 * @return List<Field>
 */
private List<Field> getParamsFields(Object params) {
    return Arrays.stream(FieldUtils.getAllFields(params))
            .filter(field -> Objects.nonNull(field.getAnnotation(SecurityField.class)))
            .collect(Collectors.toList());
}

同时考虑到解析效率问题,加解密注解处理者还持有被解析对象Class->对应标记字段Filed列表的缓存,实体完成一次解析之后,后续无需再进行加解密字段搜索

之后则是对需要加密的实体进行加密算法处理,并进行反射赋值

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 处理加密-单一实体
 *
 * @param sourceParam   源对象
 * @param encryptFields 需要加密字段集合
 * @return Object
 */
private Object processEncrypt(Object sourceParam, List<Field> encryptFields) {
    if (!REFERENCE_SET.contains(sourceParam.hashCode())) {
        for (Field field : encryptFields) {
            ReflectionUtils.makeAccessible(field);
            Object sourceObject = ReflectionUtils.getField(field, sourceParam);
            // 目前只支持String
            if (!(sourceObject instanceof String)) {
                continue;
            }
            String encryptedString = securityAlgorithm.encrypt(String.valueOf(sourceObject));
            ReflectionUtils.setField(field, sourceParam, encryptedString);
            REFERENCE_SET.add(sourceParam.hashCode());
        }
    }
    return sourceParam;
}

# 动态SQL加密插件-MybatisDynamicSqlEncryptInterceptor

普通SQL加密插件很好的解决的大部分MybatisMybatis-Plus的加密需求,但对于动态SQL光靠上述的方法是无法解决的,因为动态SQL的解析时机在ParameterHandler之前,Mybatis需要将动态标签解析为静态SQL,这一步操作是在Executor调用StatementHandler.parameterize()前做的,由MappedStatementHandler.getBoundSql(Object parameterObject)函数解析动态标签,生成静态SQL语句。由于执行阶段早,此时静态SQL已经生成,后续再拦截StatementHandlerParameterHandler中处理parameterObject进行加密都是无效的。

一个典型的动态SQL如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<insert id="batchInsertWithList" useGeneratedKeys="true" keyProperty="id">
    insert into test_version(id,name,id_card,
    phone,version)
    values
    <foreach collection="testVersionDOList" item="testVersion" separator=",">
        (#{testVersion.id,jdbcType=NUMERIC},#{testVersion.name,jdbcType=VARCHAR},#{testVersion.idCard,jdbcType=VARCHAR},
        #{testVersion.phone,jdbcType=VARCHAR},#{testVersion.version,jdbcType=NUMERIC})
    </foreach>
</insert>

所以对于动态SQL的加密,只能选择拦截Executorupdatequery方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
//        对于select请求不做处理,通常加密字段无法进行模糊查询,只能外部进行手动加密后进行等值查询,符合逻辑
//        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})

同样的

  1. 首先获取需要处理的对象
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 获取拦截器拦截的设置参数对象DefaultParameterHandler
final Object[] args = invocation.getArgs();
Object parameterObject = args[1];
if (Objects.isNull(parameterObject)) {
    return invocation.proceed();
}
  1. 对于ListMap,底层统一为Map,进行通用加密处理,不进行深拷贝
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 如果为mybatis foreach用法,外部为List入参,内部统一处理为Map
if (parameterObject instanceof Map) {
    Map<String, Object> parameterObjectMap = (Map<String, Object>) parameterObject;
    for (Map.Entry<String, Object> paramObjectEntry : parameterObjectMap.entrySet()) {
        Object value = paramObjectEntry.getValue();
        if (value != null) {
            // 此处不能进行深拷贝复制,因为新增后id的回填需要上下文同一个对象引用
            // 详情可见org.apache.ibatis.executor.statement.PreparedStatementHandler.update
            // 进行加密
            Object encryptObject = securityResolver.encryptFiled(value);
            parameterObjectMap.put(paramObjectEntry.getKey(), encryptObject);
        }
    }
    invocation.getArgs()[1] = parameterObjectMap;
}
return invocation.proceed();

加密过程和上文一致,区别在于此时不能进行深拷贝

原因是因为Mybatis在新增数据后有一个回填id的功能,其功能实现在org.apache.ibatis.executor.statement.PreparedStatementHandler#update

其中KeyGenerator将对静态SQL的新增实体回填id,此时的新增实体parameterObject和需要加密的实体是同一个,如果进行深拷贝,则加密对象为另外的一个实体,而此时id回填的为原始实体,由于原始实体已经不再使用,出参为加密实体,将造成回填id失效

虽然动态SQL并未进行深拷贝,但其执行阶段在普通SQL加密插件之前,后续经过普通SQL加密插件后,仍然能够进行深拷贝,以确保加密对象不对原始对象引用进行修改,这也是组件将动态SQL和普通SQL分离为2个插件的原因

# 解密插件-MybatisDecryptInterceptor

解密需要拦截ResultSetHandler阶段,此时是将SQL执行的真实结果转化为Java对象的时机

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets",
                args = {Statement.class})
})

本质也和加密过程差不多,核心代码为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 对字段进行解密
 *
 * @param params params
 * @return Object
 */
public Object decryptFiled(Object params) {
    if (Objects.isNull(params)) {
        return null;
    }
    // 此处没有并发问题,只是为了通过stream的lambda编译
    AtomicReference<Class<?>> clazz = new AtomicReference<>();
    AtomicReference<Object> object = new AtomicReference<>();
    // 解析返回值为List的
    if (params instanceof List) {
        List<?> sourceList = (List<?>) params;
        if (CollectionUtils.isEmpty(sourceList)) {
            return params;
        }
        sourceList.stream().filter(Objects::nonNull)
                .findFirst()
                .ifPresent(source -> {
                    clazz.set(source.getClass());
                    object.set(source);
                });
    } else {
        clazz.set(params.getClass());
        object.set(params);
    }
    if (null == clazz.get()) {
        return params;
    }
    List<Field> decryptFields = getParamsFields(object.get());
    if (!CollectionUtils.isEmpty(decryptFields)) {
        List<?> paramsList;
        if (params instanceof List) {
            paramsList = (List<?>) params;
        } else {
            paramsList = Collections.singletonList(params);
        }
        return processDecrypt(paramsList, decryptFields);
    }
    return params;
}

# 脱敏切面

脱敏需求采用AOP+自定义注解的方式实现

组件在设计时没有直接采用@Aspect注解的切面形式,而是采用Advisor+Aspect+InterceptorAop形式,目的是为了在自动注入时更好的控制切面的注入,同时预留可扩展式切点,让用户自行定义脱敏切面切点

DesensitizationAdvisor中,默认切点为@annotation(com.rpamis.security.annotation.Desensitizationed)

# 如何寻找所有需要脱敏字段

脱敏需求的核心诉求在于,对于任意类型的实体,只要实体内有被脱敏注解标记的类,都需要进行脱敏处理,其中包含了嵌套脱敏等。

所以如何获得任意实体的所有需要脱敏的字段是需要解决的首要任务

# 递归法

寻找一个对象中所有包含XXX自定义脱敏注解的方法,通常能够快速想到递归处理

基本的伪代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static List<Field> findAnnotatedFields(Object obj, Class<? extends Annotation> annotationClass) {
    List<Field> annotatedFields = new ArrayList<>();
    Class<?> clazz = obj.getClass();
    Field[] fields = clazz.getDeclaredFields();

    for (Field field : fields) {
        if (field.isAnnotationPresent(annotationClass)) {
            annotatedFields.add(field);
        }
        // 如果字段是一个嵌套的对象,则递归处理
        if (!field.getType().isPrimitive() && !field.getType().isAssignableFrom(String.class)
                && !field.getType().isEnum()) {
            field.setAccessible(true);
            try {
                Object nestedObject = field.get(obj);
                if (nestedObject != null) {
                    // 递归处理
                    annotatedFields.addAll(findAnnotatedFields(nestedObject, annotationClass));
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    return annotatedFields;
}

递归能够很快速的写出最简要的核心逻辑,但需要防止OOM的发生,同时在递归处理时需要考虑解析的对象类型,比如解析的对象是ListMapEnumArray需要的处理可能是不一样的,在考虑这种情况下进行递归代码很可能变成

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (Field field : fields) {
    if (field == null) {
        continue;
    }
    field.setAccessible(true);
    Object value = field.get(javaBean);
    if (null != value) {
        Class<?> type = value.getClass();
        if (type.isArray()) {
            replaceArray(referenceCounter, value);
        } else if (value instanceof Collection<?>) {
            if (replaceCollection(referenceCounter, (Collection<?>) value)) {
                condition = true;
            }
        } else if (value instanceof Map<?, ?>) {
            Map<?, ?> m = (Map<?, ?>) value;
            Set<?> set = m.entrySet();
            for (Object o : set) {
                Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
                Object mapVal = entry.getValue();
                if (isNotGeneralType(mapVal.getClass(), mapVal, referenceCounter)) {
                    stack.push(mapVal);
                }
            }
        } else if (value instanceof Enum<?>) {
            condition = true;
        } else {
            if (isNotJdkAndBaseType(referenceCounter, field, value, type)) {
                Field[] nestedFields = ObjectUtils.getAllFields(value);
                boolean nestedReplaced = replace(nestedFields, value, referenceCounter);
                if (nestedReplaced) {
                    condition = true;
                }
            }
        }
    }

这使得递归代码核心逻辑不清晰,且递归退出条件夹在各种类型处理过程中,对于任意需要进行拓展的处理类型,需要改动核心代码,整个组件将随着迭代变得很难维护

# 迭代法

基于递归法的缺点,利用Queue迭代是个容易想到的解决方法

改进后的迭代法伪代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Deque<Object> stack = new ArrayDeque<>();
stack.push(obj);

while (!stack.isEmpty()) {
    Object currentObj = stack.pop();

    Class<?> clazz = currentObj.getClass();
    Field[] fields = clazz.getDeclaredFields();

    for (Field field : fields) {
        if (field.isAnnotationPresent(annotationClass)) {
            maskField(currentObj, field);
        }

        if (!field.getType().isPrimitive() && !field.getType().isAssignableFrom(String.class)
                && !field.getType().isEnum()) {
            field.setAccessible(true);
            try {
                Object nestedObject = field.get(currentObj);
                if (nestedObject != null) {
                    stack.push(nestedObject);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

对于需要解析的对象,首先加入队列中,之后每发现一个需要脱敏的Field就将他放入队列,外层通过队列判空作为循环条件

同时,基于可拓展性的需要,组件定义了FieldProcessTypeHandler,其中FieldProcess主要有3个实现类

  • DataMaskingProcessor:用于处理非泛型的,带有@Masked注解的实体
  • NestedMaskingProcessor:用于处理非泛型的,带有@NestedMasked注解的实体
  • MaskingResponseProcessor:用于处理所有带泛型的实体

执行顺序由上到下,每个均会执行

同时每个Processor中都会经过TypeHandler处理,TypeHandler主要有4个实现类

  • ArrayTypeHandler:用于特殊处理Array类型的数据
  • CollectionTypeHandler:用于特殊处理Collection类型的数据
  • MapTypeHandler:用于特殊处理Map类型的数据
  • OtherTypeHandler:用于处理自定义的实体或其他非Java提供的类型数据

执行顺序同样从上到下,如果有一个能够处理,则后续不会执行

基于改进后的脱敏处理核心逻辑为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 将源对象进行脱敏返回
 *
 * @param sourceObject 源对象
 * @return Object
 */
@SuppressWarnings("all")
public static Object processObjectAndMask(Object sourceObject) throws IllegalAccessException {
    if (sourceObject == null) {
        return null;
    }
    Set<Integer> referenceSet = new HashSet<>();
    // 解析队列
    Deque<Object> analyzeDeque = new ArrayDeque<>();
    analyzeDeque.offer(findActualMaskObject(sourceObject));
    MaskFunctionFactory maskFunctionFactory = new MaskFunctionFactory();
    while (!analyzeDeque.isEmpty()) {
        Object currentObj = analyzeDeque.poll();
        Field[] fields = FieldUtils.getAllFields(currentObj);
        for (Field field : fields) {
            field.setAccessible(true);
            // 获取当前对象的对应的Field的值
            Object fieldValue = field.get(currentObj);
            if (null != fieldValue) {
                Class<?> fieldValueClass = fieldValue.getClass();
                ProcessContext processContext = new ProcessContext(currentObj, fieldValue, field,
                        fieldValueClass, referenceSet, HANDLER_LIST, analyzeDeque);
                processContext.setMaskFunctionFactory(maskFunctionFactory);
                // 字段解析并脱敏
                for (FieldProcess fieldProcess : PROCESS_LIST) {
                    fieldProcess.processField(processContext);
                }
            }
        }
    }
    return sourceObject;
}

findActualMaskObject

是对HashMap对象的特殊处理

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 找到真正需要Mask的Object,针对HashMap处理
 * 因为HashMap的key value属于静态内部类HashMap.Node
 * 这个对象判断isArray时为true,单独判断是否为HashMap.Node将无法通过编译
 * 导致后续脱敏处理失效
 *
 * @param sourceObject 源对象
 * @return Object
 */
private static Object findActualMaskObject(Object sourceObject) {
    if (sourceObject instanceof HashMap) {
        return ((HashMap<?, ?>) sourceObject).values();
    }
    return sourceObject;
}

这里之所以要前置处理是为了避免后续获取真实处理Field时,获取到了内部类的HashMap.Node,避免无法通过编译导致脱敏失败,这个条件的诞生是基于单测时的发现(也侧面说明一个组件的完善,单测是多么的重要)

其中PROCESS_LISTHANDLER_LIST

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * Filed处理者list
 */
private static final List<FieldProcess> PROCESS_LIST = new ArrayList<>();

/**
 * 类型处理者list
 */
private static final List<TypeHandler> HANDLER_LIST = new ArrayList<>();

/**
 * 必要处理者初始化
 */
static {
    PROCESS_LIST.add(new MaskingResponseProcessor());
    PROCESS_LIST.add(new NestedMaskingProcessor());
    PROCESS_LIST.add(new DataMaskingProcessor());
    HANDLER_LIST.add(new ArrayTypeHandler());
    HANDLER_LIST.add(new CollectionTypeHandler());
    HANDLER_LIST.add(new MapTypeHandler());
    HANDLER_LIST.add(new OtherTypeHandler());
}

对于需要脱敏的对象比如List

他在获取所有Field之后其实不是想象中的那样返回了认识的脱敏字段,而是返回了一堆List的属性,如图所示

这其中真正的数据在fileds[4]elementData中,其他都是不必要的取值,因此所有的类型处理器都需要判断当前处理的对象是否是真正要处理的对象

那么应该怎么做呢?

答案是com.rpamis.security.starter.utils.MaskAnnotationResolver#isNotBaseType

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 是否不是基础类型,或已经解析过
 *
 * @param clazz            当前Class
 * @param element          当前对象元素
 * @param referenceCounter 防重set
 * @return boolean
 */
public static boolean isNotBaseType(Class<?> clazz, Object element, Set<Integer> referenceCounter) {
    return !clazz.isPrimitive()
            && clazz.getPackage() != null
            && !clazz.isEnum()
            && !clazz.getPackage().getName().startsWith("java.")
            && !clazz.getPackage().getName().startsWith("javax.")
            && !clazz.getName().startsWith("java.")
            && !clazz.getName().startsWith("javax.")
            && referenceCounter.add(element.hashCode());
}

对于那些不需要处理的对象,他们都是Java本身自带的,判断是否是Java提供的类型或是否已经处理过就能够识别

以下实现类展示了FieldProcessTypeHandler的基本处理

DataMaskingProcessor

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DataMaskingProcessor implements FieldProcess {

    @Override
    @SuppressWarnings("all")
    public void processField(ProcessContext processContext) throws IllegalAccessException {
        Field field = processContext.getField();
        Object fieldValue = processContext.getFieldValue();
        Object currentObject = processContext.getCurrentObject();
        if (field.isAnnotationPresent(Masked.class)) {
            Masked annotation = field.getAnnotation(Masked.class);
            MaskFunctionFactory maskFunctionFactory = processContext.getMaskFunctionFactory();
            String maskValue = maskFunctionFactory.maskData(String.valueOf(fieldValue), annotation);
            field.set(currentObject, maskValue);
        }
    }
}

DataMaskingProcessor是真正去执行脱敏并进行赋值的处理类,而NestedMaskingProcessorMaskingResponseProcessor是为了从能够处理的TypeHandler中获取需要脱敏的对象,并加入到迭代Queue

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class NestedMaskingProcessor implements FieldProcess {

    @Override
    public void processField(ProcessContext processContext) {
        Field field = processContext.getField();
        List<TypeHandler> handlerList = processContext.getHandlerList();
        // 如果字段被标记为需要嵌套脱敏
        if (field.isAnnotationPresent(NestedMasked.class)) {
            for (TypeHandler handler : handlerList) {
                boolean handleResult = handler.handle(processContext);
                if (handleResult) {
                    break;
                }
            }
        }
    }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MaskingResponseProcessor implements FieldProcess {

    @Override
    public void processField(ProcessContext processContext) throws IllegalAccessException {
        List<TypeHandler> handlerList = processContext.getHandlerList();
        for (TypeHandler handler : handlerList) {
            boolean handleResult = handler.handle(processContext);
            if (handleResult) {
                break;
            }
        }
    }
}

由于各个TypeHandler仅是对不同类型进行解包处理,基础步骤类似

这里给出ArrayTypeHandler的实现便于理解

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ArrayTypeHandler implements TypeHandler {

    @Override
    public boolean handle(ProcessContext processContext) {
        Class<?> fieldValueClass = processContext.getFieldValueClass();
        Object fieldValue = processContext.getFieldValue();
        Set<Integer> referenceSet = processContext.getReferenceSet();
        Deque<Object> analyzeDeque = processContext.getAnalyzeDeque();
        if (fieldValueClass.isArray()) {
            int length = Array.getLength(fieldValue);
            for (int i = 0; i < length; i++) {
                Object arrayObject = Array.get(fieldValue, i);
                if (null != arrayObject && MaskAnnotationResolver.isNotBaseType(arrayObject.getClass(), arrayObject, referenceSet)) {
                    analyzeDeque.offer(arrayObject);
                }
            }
            return true;
        }
        return false;
    }
}

通常而言组件已经能够处理多种类型,如不满足需求,可以拓展FieldProcessTypeHandler即可完成其他类型处理

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-12-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
聊聊mybatis的Interceptor机制
org/apache/ibatis/plugin/Interceptor.java
code4it
2023/08/31
1630
聊聊mybatis的Interceptor机制
Rpamis-security-技术背景
企业数据安全一直是项目中需要重点关注的问题,自2021年《中华人民共和国数据安全法》发布以来,绝大多数大型企业都展开了自身项目的安全审查和整改,对于敏感数据要求加密存储、并支持脱敏显示,这样的述求在企业中日渐增多。
benym
2023/12/02
2130
你还不会搞数据脱敏?MyBatis 插件 + 注解轻松实现数据脱敏,So easy~!
根据不同的要求,我们只需要对ParameterHandler和ResultSetHandler进行切入。定义特定注解,在切入时需要检查字段中是否包含注解来是否加解密。
Java技术栈
2023/02/27
1.9K0
你还不会搞数据脱敏?MyBatis 插件 + 注解轻松实现数据脱敏,So easy~!
聊聊mybatis的Interceptor机制
org/apache/ibatis/plugin/Interceptor.java
code4it
2023/08/30
2330
从零开始实现一个MyBatis加解密插件
公司出于安全合规的考虑,需要对明文存储在数据库中的部分字段进行加密,防止未经授权的访问以及个人信息泄漏。
2020labs小助手
2022/08/23
8320
MyBatis 核心配置综述之 ParameterHandlers
MyBatis 四大核心组件我们已经了解到了两种,一个是 Executor ,它是MyBatis 解析SQL请求首先会经过的第一道关卡,它的主要作用在于创建缓存,管理 StatementHandler 的调用,为 StatementHandler 提供 Configuration 环境等。
cxuan
2019/07/31
1.1K0
MyBatis 核心配置综述之 ParameterHandlers
Mybatis源码笔记之浅析ParameterHandler
ParameterHandler是用来设置参数规则的。StatementHandler中介绍到,其SimpleExecutor中调用prepare()方法之后,接下来StatementHandler就是使用parameterize来设置参数。以SampleExecutor为例,具体代码如下:
沁溪源
2020/09/02
7440
MyBatis源码解析之基础模块—Plugin
上一章节我们一起学习了Mapper接口绑定的源码逻辑。本次我们学习MyBatis的Plugin数据源模块。
todobugs
2020/10/15
4480
mybatis 开发自定义插件,你学废了吗
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。比如执行前、执行后或者对SQL结果集处理、sql入参处理等,这样就可以在不修改mybatis源码的情况下对sql执行的过程或结果进行修改,实现了解耦。
索码理
2022/09/20
5700
mybatis 开发自定义插件,你学废了吗
MyBatis插件编写
Mybatis是一个操作数据库的工具,在一些场景下应用有些自定义的需求,在数据库整个执行流程上需有一些插入点可以接入自己的逻辑,如针对数据库敏感字段加密,分页等,因此MyBatis在设计的时候就采取发插件化的设计,可以让应用加入自己的逻辑。
心平气和
2021/01/29
6280
spring-boot-2.0.3不一样系列之源码篇 - pageHelper分页,绝对有值得你看的地方
  用过pageHelper的都知道(没用过的感觉去google下),实现分页非常简单,service实现层调用dao(mapper)层之前进行page设置,mapper.xml中不处理分页,这样就够了,就能实现分页了,具体如下
青石路
2019/03/11
8620
spring-boot-2.0.3不一样系列之源码篇 - pageHelper分页,绝对有值得你看的地方
《深入理解mybatis原理》 MyBatis的架构设计以及实例分析
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://louluan.blog.csdn.net/article/details/40422941
亦山
2019/05/25
1.5K0
mybatis拦截器详解_短信拦截器
  拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。Mybatis拦截器设计的一个初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。打个比方,对于Executor,Mybatis中有几种实现:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。这个时候如果你觉得这几种实现对于Executor接口的query方法都不能满足你的要求,那怎么办呢?是要去改源码吗?当然不。我们可以建立一个Mybatis拦截器用于拦截Executor接口的query方法,在拦截之后实现自己的query方法逻辑,之后可以选择是否继续执行原来的query方法。
全栈程序员站长
2022/09/30
1.7K0
Mybatis拦截器执行过程解析
上一篇文章 Mybatis拦截器之数据加密解密 介绍了 Mybatis 拦截器的简单使用,这篇文章将透彻的分析 Mybatis 是怎样发现拦截器以及调用拦截器 intercept 方法的
用户4172423
2019/09/04
1.3K0
Mybatis拦截器执行过程解析
mybatis-plus配置拦截器实现完整sql打印
在使用mybatis-plus(mybatis)的时候,往往需要打印完整的sql语句,然而输出的日志不是很理想:
shigen
2024/06/16
5940
mybatis-plus配置拦截器实现完整sql打印
MyBatis核心流程源码分析(上)
3.创建实体类User以及UserDAO和对应的xml文件,以及mybats主配置文件
全干程序员demo
2024/01/17
2080
MyBatis核心流程源码分析(上)
Mybatis拦截器之数据加密解密
Mybatis Interceptor 在 Mybatis 中被当作 Plugin(插件),不知道为什么,但确实是在 org.apache.ibatis.plugin 包下面
用户4172423
2019/09/04
2.5K0
Mybatis拦截器之数据加密解密
MyBatis 架构与原理深入解析,面试随便问!
本文主要讲解JDBC怎么演变到Mybatis的渐变过程,重点讲解了为什么要将JDBC封装成Mybaits这样一个持久层框架。再而论述Mybatis作为一个数据持久层框架本身有待改进之处。
搜云库技术团队
2023/09/18
6110
MyBatis 架构与原理深入解析,面试随便问!
手把手教你开发 MyBatis 分页插件
在日常开发中,小伙伴们多多少少都有用过 MyBatis 插件,松哥猜测大家用的最多的就是 MyBatis 的分页插件!不知道小伙伴们有没有想过有一天自己也来开发一个 MyBatis 插件?
江南一点雨
2024/03/06
1910
手把手教你开发 MyBatis 分页插件
mybatis拦截器
Executor,执行器,我们可以看到它包含了如下方法,说明它是一个比较全能的范围,可以做很多事情参数如处理、返回处理、重写sql等
阿超
2022/08/21
7650
mybatis拦截器
相关推荐
聊聊mybatis的Interceptor机制
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档