# 基于spring aop拦截器对数据库字段进行加密解密脱敏
1. 通过aop方式 + 方法/实体属性注解 + 反射处理加解密。
1. 基于aop方式实现,不受限与持久层框架限制,网上有些使用mybatis拦截器的方式实现(有缺点,对复杂的sql不支持,所以重新基于aop方式实现) ,代码已在mybatis plus中测试完毕。
2.使用方法:
2.1: 和java validation的用法基本一样(1.支持方法中的参数标记@DbEncryptField、2.支持类中的属性标记@DbEncryptField)
# 工作原理: aop层对“Mapper”层进行切入,切入之后:
1. // 对参数对象中的字段进行处理(对带有@DbEncryptField注解的字段处理)-加密
2. // 执行原始方法
3. // 对参数对象中的字段进行处理(对带有@DbEncryptField注解的字段处理)-解密
4. // 对返回结果进行解密处理,以返回原始值给调用方
# 加密处理后,原字段根据加密的算法进行扩容,比如sm4加密后推荐对原字段长度扩充10倍,以存储密文
# 代码实现
## 定义注解
package com.xxx; import java.lang.annotation.*; /*** * *--------------------------- * @Description: 标记需加密、解密的字段 * ***--------------------------- * @author: wulingming * @date: 2026年01月06日 14:41 * @version: v1.0 * ****--------------------------- */ @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface DbEncryptField { // /*** // * *--------------------------- // * @Description: 加密方式,有多种加密方式时可以指定 // * **--------------------------- // * @return EncryptTypeEnum // * ***--------------------------- // * @author: wulingming // * @date: 2026年01月06日 14:41 // * @version: v1.0 // * ****--------------------------- // */ // EncryptTypeEnum algorithm() default EncryptTypeEnum.SM4; }
## aop层切面
package com.broker.framework.mybatis.core.aspect; import com.broker.framework.mybatis.core.util.DbEncryptFieldUtil; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.Arrays; /** * * @desc 拦截方法的入参信息和返回信息 */ @Aspect @Configuration @Slf4j public class DbEncryptFieldAop { // 使用配置的密钥 @Value("${broker.mybatis.encrypt.encrypt-key}") private String encryptKey; @Pointcut(value = "execution(* com.broker..*Mapper.*(..))") private void mapper() { } /*** * *--------------------------- * @Description: 拦截Mapper方法,对带有@DbEncryptField注解的字段和参数进行加解密处理 * 1. 在方法执行前,对方法参数和参数对象中的加密字段进行加密 * 2. 执行原始方法 * 3. 在方法执行后,对参数和返回结果进行解密,以恢复原始值 * ***--------------------------- * @author: wulingming * @date: 2026年01月05日 14:37 * @version: v1.0 * ****--------------------------- */ @Around(value = "mapper()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 校验加密密钥 if (!StringUtils.hasText(encryptKey)) { log.error("加密密钥未配置或为空,无法进行加解密操作"); throw new IllegalArgumentException("加密密钥未配置或为空"); } // 获取方法签名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Parameter[] parameters = method.getParameters(); // 获取方法参数值 Object[] objectsArray = joinPoint.getArgs(); // 对参数对象中的字段进行处理(对带有@DbEncryptField注解的字段处理)-加密 DbEncryptFieldUtil.encryptOrDecryptParameters(parameters, objectsArray, DbEncryptFieldUtil.OperationEnum.ENCRYPT, encryptKey); // 执行原始方法 Object resultObj = joinPoint.proceed(objectsArray); // 对参数对象中的字段进行处理(对带有@DbEncryptField注解的字段处理)-解密 DbEncryptFieldUtil.encryptOrDecryptParameters(parameters, objectsArray, DbEncryptFieldUtil.OperationEnum.DECRYPT, encryptKey); // 检查resultObj方法返回结果是否和入参的内存地址一致,如果一致则不再解密避免重复解密(在上一步的参数解密已经处理过一次) if (Arrays.stream(objectsArray).anyMatch(param -> resultObj == param)) { return resultObj; } Object[] objects = {resultObj}; // 对返回结果进行解密处理,以返回原始值给调用方 DbEncryptFieldUtil.encryptOrDecryptParameters(null, objects, DbEncryptFieldUtil.OperationEnum.DECRYPT, encryptKey); return objects[0]; } }
## 加解密处理工具(加密方式默认使用sm4,如需替换可手动更改doEncryptOrDecryptSm4ToString 的实现)
package com.broker.framework.mybatis.core.util; import cn.hutool.core.collection.CollUtil; import com.broker.framework.common.crypt.annotation.DbEncryptField; import com.broker.framework.common.util.sm4.SM4Util; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.util.Assert; import javax.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Parameter; import java.util.*; import java.util.function.Consumer; /** * @desc 对带有@DbEncryptField注解({@link DbEncryptField})的字段、方法参数进行加密、解密 。 * 通过aop切面切入,{@link com.broker.framework.mybatis.core.aspect.DbEncryptFieldAop} * */ @Slf4j public class DbEncryptFieldUtil { /** * The enum Encrypt operation. */ public enum OperationEnum { /** *Encrypt operation. */ ENCRYPT, /** *Decrypt operation. */ DECRYPT } /*** * *--------------------------- * @Description: 对参数进行加解密 * **--------------------------- * @param parameters 参数对象数组 * @param paramValues 参数值数组 * @param operationEnum 加解密操作枚举 * @param secretHexKey 16进制密钥(忽略大小写) * @return void * ***--------------------------- * @author: wulingming * @date: 2026年01月05日 11:44 * @version: v1.0 * ****--------------------------- */ public static void encryptOrDecryptParameters(Parameter[] parameters, Object[] paramValues, OperationEnum operationEnum, String secretHexKey) { for (int i = 0; i < paramValues.length; i++) { //返回结果没有参数名,只有返回值,这里要做空值检查。 Parameter parameter = parameters == null ? null : parameters[i]; Object paramValue = paramValues[i]; boolean inheritedAnnotation = parameter != null && parameter.isAnnotationPresent(DbEncryptField.class); //参数名 String parameterName = parameter == null ? "方法返回结果" : parameter.getName(); if (paramValue != null) { int finalI = i; encryptOrDecryptParameter(parameterName, paramValue, operationEnum, secretHexKey, inheritedAnnotation, s -> { paramValues[finalI] = s; }); } else { log.trace("[{}] 参数索引: {}, 参数名: {} 的值为 null,跳过处理", operationEnum, i, parameterName); } } } /*** * *--------------------------- * @Description: 处理参数对象,对带有@DbEncryptField注解的字段进行加密、解密 (递归方法主体) * **--------------------------- * @param fieldName 字段名 * @param fieldValue 字段值 * @param operationEnum 加解密操作枚举 * @param secretHexKey 16进制密钥(忽略大小写) * @param inheritedAnnotation 是否继承了父级的@DbEncryptField注解 * @param resultValueConsumer 结果值消费者 * @return void * ***--------------------------- * @author: wulingming * @date: 2026年01月06日 14:19 * @version: v1.0 * ****--------------------------- */ public static void encryptOrDecryptParameter(@Nullable String fieldName, @Nullable Object fieldValue, OperationEnum operationEnum, String secretHexKey, boolean inheritedAnnotation, Consumer<String> resultValueConsumer) { Assert.notNull(operationEnum, "operationEnum cannot be null"); Assert.notNull(secretHexKey, "secretHexKey cannot be null"); Assert.notNull(resultValueConsumer, "resultValueConsumer cannot be null"); if (fieldValue == null) { log.trace("[{}] 字段: {} 值为 null,跳过处理", operationEnum, fieldName); return; } if (isPrimitiveOrWrapper(fieldValue.getClass())) { if (fieldValue instanceof String) { if (inheritedAnnotation) { String resultValue = doEncryptOrDecryptSm4ToString(fieldName, (String) fieldValue, operationEnum, secretHexKey); resultValueConsumer.accept(resultValue); } else { log.trace("[{}] 字段: {} (值: {}) 未标记 @DbEncryptField 注解,跳过处理", operationEnum, fieldName, fieldValue); } } else { log.trace("[{}] 字段: {} (值: {}, 类型: {}) 是基本类型且非字符串,跳过处理", operationEnum, fieldName, fieldValue, fieldValue.getClass().getSimpleName()); } } // 检查对象是否是集合 else if (fieldValue instanceof Collection) { Collection<Object> collection = (Collection<Object>) fieldValue; if (CollUtil.isNotEmpty(collection)) { List<Object> updatedList = new ArrayList<>(); Iterator<Object> iterator = collection.iterator(); int i = 0; while (iterator.hasNext()) { Object item = iterator.next(); encryptOrDecryptParameter(fieldName + "[" + i + "]", item, operationEnum, secretHexKey, inheritedAnnotation, s -> { //先存放到list中,迭代完再添加到原集合中 updatedList.add(s); iterator.remove(); }); i++; } if (!updatedList.isEmpty()){ //迭代完之后替换值 collection.addAll(updatedList); } } else { log.trace("[{}] 集合字段: {} 为空,跳过处理", operationEnum, fieldName); } } // 检查对象是否是Map else if (fieldValue instanceof Map) { Map<Object, Object> map = (Map<Object, Object>) fieldValue; if (CollUtil.isNotEmpty(map)) { Map<Object, Object> updateMap = new HashMap<>(); map.forEach((key, value) -> encryptOrDecryptParameter(fieldName + "." + key, value, operationEnum, secretHexKey, inheritedAnnotation, s -> { updateMap.put(key, s); })); if (!updateMap.isEmpty()){ //迭代完之后替换值 map.putAll(updateMap); } } else { log.trace("[{}] Map字段: {} 为空,跳过处理", operationEnum, fieldName); } } // 普通对象 else { encryptOrDecryptObject(fieldName, fieldValue, operationEnum, secretHexKey); } } /** * 处理对象中的字段,对带有@DbEncryptField注解的字段进行加解密 * * @param fieldName 字段名 * @param fieldValue 要处理的对象 * @param operationEnum 加解密操作类型 * @param secretHexKey 加密密钥 */ @SneakyThrows public static void encryptOrDecryptObject(String fieldName, Object fieldValue, OperationEnum operationEnum, String secretHexKey) { if (fieldValue == null) { log.trace("[{}] 字段对象: {} 值为 null,跳过处理", operationEnum, fieldName); return; } //兼容外部某些jar包。 boolean isUnmodifiedJar = false; String className = fieldValue.getClass().getName(); if (className.startsWith("com.baomidou.mybatisplus.extension.plugins.pagination.Page")//兼容mybatis-plus分页对象 ) { isUnmodifiedJar = true; } Class<?> fieldValueClazz = fieldValue.getClass(); Field[] subFields = fieldValueClazz.getDeclaredFields(); for (Field subField : subFields) { subField.setAccessible(true); Object subFieldValue = subField.get(fieldValue); String currentFieldName = fieldName + "." + subField.getName(); // 检查字段是否有@DbEncryptField注解 boolean hasAnnotation = subField.isAnnotationPresent(DbEncryptField.class); if (hasAnnotation || isUnmodifiedJar) { encryptOrDecryptParameter(currentFieldName, subFieldValue, operationEnum, secretHexKey, true, s -> { try { subField.set(fieldValue, s); } catch (IllegalAccessException e) { log.error("[{}] 设置字段值失败: {}.{},错误信息: {}", operationEnum, fieldValue.getClass().getSimpleName(), subField.getName(), e.getMessage(), e); throw new RuntimeException(e); } }); } else { log.trace("[{}] 字段: {} (值: {}, 类型: {}) 未标记 @DbEncryptField 注解,跳过处理", operationEnum, currentFieldName, subFieldValue, subField.getType().getSimpleName()); } } } /*** * *--------------------------- * @Description: 对参数对象进行加解密处理,返回加密、解密的结果 * ***--------------------------- * @author: wulingming * @date: 2026年01月05日 15:58 * @version: v1.0 * ****--------------------------- */ public static String doEncryptOrDecryptSm4ToString(String fieldName, String fieldValue, OperationEnum operationEnum, String secretHexKey) { if (fieldValue != null && !fieldValue.isEmpty()) { String resultValue; // 使用SM4进行加密或解密 if (operationEnum == OperationEnum.ENCRYPT) { resultValue = SM4Util.encryptEcb(fieldValue, secretHexKey); log.trace("[{}] 字段: {} 加密完成: {} -> {}", operationEnum, fieldName, fieldValue, resultValue); } else { // DECRYPT resultValue = SM4Util.decryptEcb(fieldValue, secretHexKey); log.trace("[{}] 字段: {} 解密完成: {} -> {}", operationEnum, fieldName, fieldValue, resultValue); } return resultValue; } else { log.trace("[{}] 字段: {} 值为 {},跳过加解密处理", operationEnum, fieldName, fieldValue); return fieldValue; } } /** * 判断是否为基础类型或包装类型 */ private static boolean isPrimitiveOrWrapper(Class<?> clazz) { return clazz.isPrimitive() || clazz == String.class || clazz == Boolean.class || clazz == Character.class || clazz == Byte.class || clazz == Short.class || clazz == Integer.class || clazz == Long.class || clazz == Float.class || clazz == Double.class; } }
浙公网安备 33010602011771号