2团
Published on 2025-06-05 / 4 Visits
0
0

MyBatis-Plus的LambdaUpdateWrapper通过Interceptor实现字段加密的考量

1. 前言

在项目中,当使用 LambdaUpdateWrapper 执行 update 操作时,发现字段无法像实体保存那样自动加密,必须手动赋值加密后的值。于是深入分析了加密拦截器的实现逻辑。

2. 拦截器实现

2.1 拦截器实现

拦截器关注update操作,代码大致如下:

  private final EncryptUtil encryptUtil;

    // SoftReference缓存加密字段
    private static final Map<Class<?>, SoftReference<List<Field>>> ENCRYPT_FIELD_CACHE =
        new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        Set<Object> processed = Collections.newSetFromMap(new IdentityHashMap<>());

        for (Object arg : args) {
            if (arg instanceof MappedStatement) {
                continue;
            }

            if (arg instanceof Map<?, ?> map) {
                if (map.isEmpty()) {
                    return arg;
                }
                for (Map.Entry<?, ?> entry : map.entrySet()) {
                    var key = entry.getKey();
                    var value = entry.getValue();
                    if (key.toString().startsWith("param") || key.toString().startsWith("et")
                        || key.toString().startsWith("collection")) {
                        if (value instanceof ArrayList<?> list) {
                            for (Object item : list) {
                                encryptField(item, processed);
                            }
                        } else {
                            encryptField(value, processed);
                        }
                    }
                }
            } else {
                encryptField(arg, processed);
            }
        }
        return invocation.proceed();
    }

    /**
     * 加密对象的所有加密字段,避免嵌套加密
     */
    private void encryptField(Object object, Set<Object> processed) throws IllegalAccessException {
        if (object == null || processed.contains(object)) {
            return;
        }
        processed.add(object);

        List<Field> encryptFields = getEncryptFields(object.getClass());
        for (Field field : encryptFields) {
            Object value = field.get(object);
            if (value != null) {
                String encrypted = encryptUtil.mpEncrypt(value.toString());
                field.set(object, encrypted);
                log.debug("字段 [{}] 已加密,原值: [{}], 加密后: [{}]", field.getName(), value,
                    encrypted);
            }
        }
    }

    /**
     * 获取并缓存带有@Encrypt注解的字段
     */
    private List<Field> getEncryptFields(Class<?> clazz) {
        SoftReference<List<Field>> ref = ENCRYPT_FIELD_CACHE.get(clazz);
        List<Field> fields = ref != null ? ref.get() : null;
        if (fields == null) {
            fields = new ArrayList<>();
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(Encrypt.class)) {
                    field.setAccessible(true);
                    fields.add(field);
                }
            }
            ENCRYPT_FIELD_CACHE.put(clazz, new SoftReference<>(fields));
            log.debug("缓存类 [{}] 的加密字段: {}", clazz.getName(),
                fields.stream().map(Field::getName).toList());
        }
        return fields;
    }

在当前代码的实现中,intercept函数接收到arg参数LambdaUpdateWrapper类型时,无法通过反射机制正常获取字段值(具体详见下文),进而无法执行加密操作。

2.2 LambdaUpdateWrapper 加密实现

private void encryptLambdaUpdateWrapper(LambdaUpdateWrapper<?> wrapper, EncryptUtil encryptUtil)
    throws IllegalAccessException {

    Object entity = wrapper.getEntity();
    List<Field> encryptFields = null;
    if (entity != null) {
        encryptFields = getEncryptFields(entity.getClass());
        for (Field field : encryptFields) {
            Object value = field.get(entity);
            if (value != null) {
                String encrypted = encryptUtil.mpEncrypt(value.toString());
                field.set(entity, encrypted);
                log.debug("LambdaUpdateWrapper实体字段 [{}] 已加密,原值: [{}], 加密后: [{}]",
                    field.getName(), value, encrypted);
            }
        }
    }

    // 处理set方法设置的字段
    List<String> sqlSet = (List<String>) ReflectUtil.getFieldValue(wrapper, "sqlSet");
    if (sqlSet != null && entity != null) {
        // 构建加密字段名映射,避免多次遍历
        Map<String, Field> encryptFieldMap = new java.util.HashMap<>();
        for (Field field : encryptFields) {
            encryptFieldMap.put(field.getName(), field);
        }
        for (int i = 0; i < sqlSet.size(); i++) {
            String setExpr = sqlSet.get(i); // 形如 name='xxx'
            String[] parts = setExpr.split("=", 2);
            if (parts.length == 2) {
                String fieldName = parts[0].trim();
                Field field = encryptFieldMap.get(fieldName);
                if (field != null) {
                    String value = parts[1].replaceAll("'", "").trim();
                    String encrypted = encryptUtil.mpEncrypt(value);
                    sqlSet.set(i, fieldName + "='" + encrypted + "'");
                    log.debug("LambdaUpdateWrapper setSql字段 [{}] 已加密,原值: [{}], 加密后: [{}]",
                        fieldName, value, encrypted);
                }
            }
        }
    }
}

针对LambdaUpdateWrapper参数的加密处理逻辑,主要包含以下两个关键部分:

  1. 实体对象处理:由于LambdaUpdateWrapper对实体对象进行了封装,需要先提取出其中封装的原始entity对象。通过反射机制获取实体类中标记了加密注解的字段,并对这些字段的值进行加密处理。这一步骤确保了通过set(entity)方式设置的字段能够被正确加密。

  2. SQL片段处理:LambdaUpdateWrapper内部维护了一个sqlSet属性(List<String>类型),它会将实体字段引用转换为数据库列名,并与对应的值拼接成SQL片段(如"username='admin'")。处理时需要解析这些SQL片段,通过字段名反向查找实体类中的对应属性,判断是否包含加密注解,进而决定是否需要对值部分进行加密处理。这一步骤确保了通过setSql()等直接拼接SQL的方式设置的字段值也能被加密。

3. 放弃

虽然针对LambdaUpdateWrapper设计了完整的加密处理方案,但经过深入评估后决定放弃实现,主要基于以下考虑:

  1. 字段映射关系缺失:由于MyBatis-Plus的字段名转换机制(如驼峰命名userName转换为下划线user_name,开发人员创建数据表字段别名等),无法准确建立数据库列名与实体属性名的双向映射关系,导致无法可靠识别需要加密的字段。

  2. 性能损耗问题:方案涉及大量SQL字符串的解析、分割和重组操作,这种基于字符串处理的方式会带来显著的性能开销,特别是在高频更新场景下可能成为系统瓶颈。

  3. 维护成本较高:该方案需要深度依赖LambdaUpdateWrapper的内部实现细节(如sqlSet字段结构),这些未公开的实现可能在后续版本变更时导致兼容性问题,增加长期维护成本。


Comment