基于MyBatis自定义拦截器实现数据库字段加密脱敏


整体加解密思路:通过 MyBatis 的拦截器,在插入库前和查询结果后对有注解的字段,进行相应的加解密处理。

项目目录:
在这里插入图片描述

文件说明:

  • @DbSafe:数据库安全注解
  • DbSafeManager:
    1. 读取配置的包路径
    2. 获取包路径下所有 .class 文件的资源对象
    3. 递归遍历类及其父类的字段
    4. 过滤标注 @DbSafe 注解的目标字段
    5. 如果有,就要把这个类和对应的字段存入全局 ConcurrentHashMap<Class<?>, Set<Field>> 进行缓存
  • MybatisInputInterceptor
    1. 拦截并处理 ParameterHandler 接口的 setParameters 方法
    2. 获取原始参数对象,判断对象是否在全局 ConcurrentHashMap
    3. 从缓存中获取需要加密的字段,反射获取原始值,加密后写回字段。
  • MybatisOutputInterceptor
    1. 拦截 ResultSetHandler 类的 handleResultSets 方法
    2. 获取结果集对象,判断对象是否在全局 ConcurrentHashMap
    3. 从缓存中获取需要解密的字段,反射获取原始值,解密后写回字段。
  • DbSafeAutoConfiguration:注入 DbSafeManagerMybatisInputInterceptorMybatisOutputInterceptor
    • 可以通过 @AutoConfiguration(after = MybatisPlusConfig.class) 自定义 MyBatis 配置,此处可选,本例没有使用
  • DbSafeStrategy:通过 枚举类 的方式实现了加解密策略,最核心的思想就是函数式编程。这个 Function,它是 java.util.function 包下的。
  • EncryptUtil:加解密工具类
  • application.yml
    1. typeAliasesPackage: com.ah.ums.service.**.entity:配置包路径
    2. mybatis-extends.db-safe.enable: true:数据库字段加密 开关,考虑 Mybatis 的拦截器对性能有所消耗,所以在没有加解密的场景时可以关闭的。

心得:

  • 如果想要简单,只需要@DbSafe、MybatisInputInterceptor、MybatisOutputInterceptor、DbSafeAutoConfiguration 即可完成需求。
  • DbSafeManager:是为了启动时将标注的类字段进行缓存,增加运行效率
  • DbSafeStrategy:策略枚举类结合函数式编程的方式可以应用到很多场景,增加代码维护性
  • MybatisPlusConfig:自定义 MyBatis 配置,读取 yaml 类型配置文件

1. 新增注解@DbSafe

package com.chinaums.security.annotation;
/**
 * 数据库安全注解
 * 支持字段的自动加解密和存入时脱敏
 */
@Documented
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbSafe {

    /**
     * 安全策略
     */
    DbSafeStrategy strategy() default DbSafeStrategy.ENCRYPT_SM4;

    boolean encrypt() default true;

    boolean decrypt() default true;
}

2. DbSafeManager

这个 Manager 的最主要判断类中是否含有 @DbSafe 注解的字段。如果有就要把这个类和对应的字段 缓存 起来,并且调用注解中策略进行加解密操作。

package com.chinaums.security.manager;
/**
 * 加密管理类
 */
@Slf4j
@AllArgsConstructor
public class DbSafeManager {

    /**
     * 类加密字段缓存
     */
    private Map<Class<?>, Set<Field>> fieldCache = new ConcurrentHashMap<>();

    /**
     * 构造方法传入实体类包
     *
     * @param typeAliasesPackage 实体类包
     */
    public DbSafeManager(String typeAliasesPackage) {
        scanDbSafeClasses(typeAliasesPackage);
    }


    /**
     * 获取类加密字段缓存
     */
    public Set<Field> getFieldCache(Class<?> sourceClazz) {
        if (ObjectUtil.isNotNull(fieldCache)) {
            return fieldCache.get(sourceClazz);
        }
        return null;
    }


    public String encrypt(String value, Field field) {
        DbSafe dbSafe = field.getAnnotation(DbSafe.class);
        return !dbSafe.encrypt() ? value : dbSafe.strategy().encryptor().apply(value);
    }


    public String decrypt(String value, Field field) {
        DbSafe dbSafe = field.getAnnotation(DbSafe.class);
        return !dbSafe.decrypt() ? value : dbSafe.strategy().decryptor().apply(value);
    }

    /**
     * 通过 typeAliasesPackage 设置的扫描包 扫描缓存实体
     */
    private void scanDbSafeClasses(String typeAliasesPackage) {
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
        String splitRegex = "[:,]";
        String[] packagePatternArray = typeAliasesPackage.split(splitRegex);
        String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
        try {
            for (String packagePattern : packagePatternArray) {
                String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
                Resource[] resources = resolver.getResources(classpath + path + "/*.class");
                for (Resource resource : resources) {
                    ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
                    Class<?> clazz = Resources.classForName(classMetadata.getClassName());
                    Set<Field> encryptFieldSet = getSafeFieldSetFromClazz(clazz);
                    if (CollUtil.isNotEmpty(encryptFieldSet)) {
                        fieldCache.put(clazz, encryptFieldSet);
                    }
                }
            }
        } catch (Exception e) {
            log.error("初始化数据安全缓存时出错:{}", e.getMessage());
        }
    }

    /**
     * 获得一个类的安全字段集合
     */
    private Set<Field> getSafeFieldSetFromClazz(Class<?> clazz) {
        Set<Field> fieldSet = new HashSet<>();
        // 判断clazz如果是接口,内部类,匿名类就直接返回
        if (clazz.isInterface() || clazz.isMemberClass() || clazz.isAnonymousClass()) {
            return fieldSet;
        }
        while (clazz != null) {
            Field[] fields = clazz.getDeclaredFields();
            fieldSet.addAll(Arrays.asList(fields));
            clazz = clazz.getSuperclass();
        }
        fieldSet = fieldSet.stream().filter(field ->
                        field.isAnnotationPresent(DbSafe.class) && field.getType() == String.class)
                .collect(Collectors.toSet());
        for (Field field : fieldSet) {
            field.setAccessible(true);
        }
        return fieldSet;
    }

}

代码拆解:

1. 处理包路径配置

String splitRegex = "[:,]";
String[] packagePatternArray = typeAliasesPackage.split(splitRegex);

我们配置的 typeAliasesPackagecom.ah.ums.service.**.entity

2. 类路径扫描

PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;   // classpath: classpath*:
String path = ClassUtils.convertClassNameToResourcePath(packagePattern);  // path: com/ah/ums/service/**/entity
Resource[] resources = resolver.getResources(classpath + path + "/*.class");
  • PathMatchingResourcePatternResolver resolver:用于解析类路径资源,支持Ant风格路径匹配(如**/*.class)。
  • ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX:类路径前缀 classpath*:
  • ClassUtils.convertClassNameToResourcePath(packagePattern):将包名转换为文件路径(如com.ah.ums.service.**.entitycom/ah/ums/service/**/entity)。
  • resolver.getResources(classpath + path + “/*.class”):获取包路径下所有 .class 文件的资源对象。
    在这里插入图片描述

3. 类元数据读取与处理

CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
for (Resource resource : resources) {
    ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
    Class<?> clazz = Resources.classForName(classMetadata.getClassName());
    Set<Field> encryptFieldSet = getSafeFieldSetFromClazz(clazz);
    if (CollUtil.isNotEmpty(encryptFieldSet)) {
        fieldCache.put(clazz, encryptFieldSet);
    }
}
  • CachingMetadataReaderFactory factory:用于读取类元数据(如类名、方法等)并缓存,提升性能。
  • factory.getMetadataReader(resource):读取类元数据(无需加载类)。
    在这里插入图片描述
  • classMetadata.getClassName():获取完整类名(如 com.ah.ums.service.gasazjPos.entity.FailPayRecordEntity)。
  • Resources.classForName(…):加载类到 JVM 中。
  • getSafeFieldSetFromClazz(clazz):获取该类中需要加密的字段集合,若字段集合非空,将其存入 fieldCache

4. 过滤无效类类型

if (clazz.isInterface() || clazz.isMemberClass() || clazz.isAnonymousClass()) {
    return fieldSet; // 直接返回空集合
}

跳过接口/内部类/匿名类,因为接口没有字段。

5. 递归遍历类及其父类的字段

while (clazz != null) {
    Field[] fields = clazz.getDeclaredFields(); // 获取当前类所有字段(包括私有)
    fieldSet.addAll(Arrays.asList(fields));     // 添加到集合
    clazz = clazz.getSuperclass();              // 向上遍历父类
}

6. 过滤标注 @DbSafe 注解的目标字段

fieldSet = fieldSet.stream()
    .filter(field -> 
        field.isAnnotationPresent(DbSafe.class) && // 有@DbSafe注解
        field.getType() == String.class            // 字段类型为String
    )
    .collect(Collectors.toSet());

仅处理String类型字段,因为用于加密文本数据,只支持字符串。

7. 设置字段可访问

for (Field field : fieldSet) {
    field.setAccessible(true); // 突破私有访问限制
}

即使字段是private的,后续代码也可通过反射读写。

3. 输入拦截器 MybatisInputInterceptor

这里通过使用 Mybatis 框架的拦截器实现对字段的加密动作,这里拦截并处理 ParameterHandler 接口的 setParameters 方法。作用:设置参数值时调用加密

package com.chinaums.security.interceptor;
/**
 * Mybatis入参拦截器
 *
 */
@Slf4j
@Intercepts({@Signature(
        type = ParameterHandler.class,
        method = "setParameters",
        args = {PreparedStatement.class})
})
@AllArgsConstructor
public class MybatisInputInterceptor implements Interceptor {

    private final DbSafeManager dbSafeManager;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        Object parameterObject = parameterHandler.getParameterObject();
        if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) {
            this.safeInputHandler(parameterObject);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {return Plugin.wrap(target, this);}

    @Override
    public void setProperties(Properties properties) {}

    /**
     * 对字段进行安全处理
     *
     * @param sourceObject 原对象
     */
    private void safeInputHandler(Object sourceObject) {
        if (ObjectUtil.isNull(sourceObject)) {
            return;
        }
        if (sourceObject instanceof Map<?, ?>) {
            Map<String, Object> map = (Map<String, Object>) sourceObject;
            new HashSet<>(map.values()).forEach(this::safeInputHandler);
            return;
        }
        if (sourceObject instanceof List<?>) {
            List<Object> list = (List<Object>) sourceObject;
            if(CollUtil.isEmpty(list)) {
                return;
            }
            // 判断第一个元素是否含有注解。如果没有直接返回,提高效率
            Object firstItem = list.get(0);
            if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(dbSafeManager.getFieldCache(firstItem.getClass()))) {
                return;
            }
            list.forEach(this::safeInputHandler);
            return;
        }
        // 是不是在缓存中的需要数据安全的类
        Set<Field> fields = dbSafeManager.getFieldCache(sourceObject.getClass());
        if(ObjectUtil.isNull(fields)){
            return;
        }
        try {
            for (Field field : fields) {
                field.set(sourceObject, this.dbSafeManager.encrypt(Convert.toStr(field.get(sourceObject)), field));
            }
        } catch (Exception e) {
            log.error("处理安全字段时出错", e);
        }
    }
}

safeInputHandler 代码拆解:
实现了一个嵌套对象结构的敏感字段加密处理器,核心逻辑是通过 递归 遍历Map、List和对象字段,结合反射对标记字段进行加密。

1. 空对象处理

if (ObjectUtil.isNull(sourceObject)) return;

2. 处理Map类型

if (sourceObject instanceof Map<?, ?>) {
    Map<String, Object> map = (Map<String, Object>) sourceObject;
    new HashSet<>(map.values()).forEach(this::safeInputHandler); // 去重后递归处理Value
    return;
}
  • 去重优化:通过 new HashSet<>(map.values())Value 去重,避免重复处理相同对象(如多个 Key 指向同一个 Value)。

3. 处理List类型

if (sourceObject instanceof List<?>) {
    List<Object> list = (List<Object>) sourceObject;
    if (CollUtil.isEmpty(list)) return;

    // 优化:若首个元素无需处理,直接跳过整个List
    Object firstItem = list.get(0);
    if (ObjectUtil.isNull(firstItem) || 
       CollUtil.isEmpty(dbSafeManager.getFieldCache(firstItem.getClass()))) {
        return;
    }
    list.forEach(this::safeInputHandler); // 递归处理每个元素
    return;
}

4. 处理普通对象

// 检查是否为需要加密的类
Set<Field> fields = dbSafeManager.getFieldCache(sourceObject.getClass());
if (ObjectUtil.isNull(fields)) return;

// 遍历加密字段并加密值
try {
    for (Field field : fields) {
        Object originalValue = field.get(sourceObject);
        String strValue = Convert.toStr(originalValue); // 转换为字符串(可能丢失类型信息)
        String encryptedValue = dbSafeManager.encrypt(strValue, field); // 加密
        field.set(sourceObject, encryptedValue); // 反射设置字段值
    }
} catch (Exception e) {
    log.error("处理安全字段时出错", e);
}
  • 加密逻辑:从缓存中获取需要加密的字段,反射获取原始值,加密后写回字段。

4. 输出拦截器 MybatisOutputInterceptor

Mybatis 输出拦截器,用于拦截 ResultSetHandler 类的 handleResultSets 方法。作用:返回字段值时调用解密

package com.chinaums.security.interceptor;
/**
 * Mybatis出参拦截器
 */
@Slf4j
@Intercepts({@Signature(
        type = ResultSetHandler.class,
        method = "handleResultSets",
        args = {Statement.class})
})
@AllArgsConstructor
public class MybatisOutputInterceptor implements Interceptor {

    private final DbSafeManager dbSafeManager;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取执行mysql执行结果
        Object result = invocation.proceed();
        if (result == null) {
            return null;
        }
        safeOutputHandler(result);
        return result;
    }

    @Override
    public Object plugin(Object target) {return Plugin.wrap(target, this);}

    @Override
    public void setProperties(Properties properties) {}

    /**
     * 输出对象
     *
     * @param sourceObject 待输出对象
     */
    private void safeOutputHandler(Object sourceObject) {
        if (ObjectUtil.isNull(sourceObject)) {
            return;
        }
        if (sourceObject instanceof Map<?, ?>) {
            Map<?, ?> map = (Map<?, ?>) sourceObject;
            new HashSet<>(map.values()).forEach(this::safeOutputHandler);
            return;
        }
        if (sourceObject instanceof List<?>) {
            List<?> list = (List<?>) sourceObject;
            if (CollUtil.isEmpty(list)) {
                return;
            }
            // 判断第一个元素是否含有注解。如果没有直接返回,提高效率
            Object firstItem = list.get(0);
            if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(dbSafeManager.getFieldCache(firstItem.getClass()))) {
                return;
            }
            list.forEach(this::safeOutputHandler);
            return;
        }
        Set<Field> fields = dbSafeManager.getFieldCache(sourceObject.getClass());
        if (ObjectUtil.isNull(fields)) {
            return;
        }
        try {
            for (Field field : fields) {
                field.set(sourceObject, this.dbSafeManager.decrypt(Convert.toStr(field.get(sourceObject)), field));
            }
        } catch (Exception e) {
            log.error("处理安全字段时出错", e);
        }
    }
}

5. 自动配置类

注入 DbSafeManagerMybatisInputInterceptorMybatisOutputInterceptor

package com.chinaums.security.config;
/**
 * 数据安全的配置类
 */
// @AutoConfiguration(after = MybatisPlusConfig.class)
@Configuration
@ConditionalOnProperty(value = "mybatis-extends.db-safe.enable", havingValue = "true")
@Slf4j
public class DbSafeAutoConfiguration {
    @Bean
    public DbSafeManager safeManager(MybatisPlusProperties mybatisPlusProperties) {
        return new DbSafeManager(mybatisPlusProperties.getTypeAliasesPackage());
    }

    @Bean
    public MybatisInputInterceptor inputInterceptor(DbSafeManager safeManager) {
        return new MybatisInputInterceptor(safeManager);
    }

    @Bean
    public MybatisOutputInterceptor outputInterceptor(DbSafeManager safeManager) {
        return new MybatisOutputInterceptor(safeManager);
    }

}

说明:

@ConditionalOnProperty(value = "mybatis-extends.db-safe.enable", havingValue = "true")

这里考虑 Mybatis 的拦截器对性能有所消耗,所以在没有加解密的场景时可以关闭的。对应在 application.yml 的配置为:

# 数据库字段加密
mybatis-extends:
  # 是否开启加密
  db-safe:
    enable: true

enabletrue 时,该配置类才会被执行,这里的3个Bean才会被创建出来。

👉mybatisPlusProperties.getTypeAliasesPackage() 获取 application.yml 文件中 typeAliasesPackage

# Mybatis Plus配置
mybatis-plus:
  # mapper XML 文件位置
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # 实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.chinaums.capacity.**.entity,com.chinaums.capacity.**.vo
  global-config:
    dbConfig:
      # 主键类型
      # AUTO:自增; NONE:空; INPUT:用户输入;
      # ASSIGN_ID:雪花(默认); ASSIGN_UUID: 唯一UUID
      idType: ASSIGN_ID

👉引申:
MyBatis 的默认配置是在其 org.apache.ibatis.session.Configuration 类的构造方法中初始化的。当没有显式提供配置文件(如 mybatis-config.xml)时,MyBatis 会自动使用这些默认配置。

Spring Boot项目中,MyBatis 通常与 Spring Boot 的自动配置功能结合使用,读取 application.yml(或application.properties)中的配置。

也可以自定义配置文件进行 MyBatis 配置,示例如下:

@AutoConfiguration(after = MybatisPlusConfig.class)
@ConditionalOnProperty(value = "mybatis-extends.db-safe.enable", havingValue = "true")
@Slf4j
public class DbSafeAutoConfiguration { ... }

👉指定 DbSafeAutoConfiguration 需要在 MybatisPlusConfig 后面自动配置注入。

5.1 mybatis-plus配置类 MybatisPlusConfig

package com.chinaums.database.config;
/**
 * mybatis-plus配置类
 */
@EnableTransactionManagement(proxyTargetClass = true)
@AutoConfiguration
@PropertySource(value = "classpath:default-mybatis-plus.yaml", factory = YmlPropertySourceFactory.class)
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor());
        // 乐观锁插件
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
        // 阻断插件
        interceptor.addInnerInterceptor(blockAttackInnerInterceptor());
        return interceptor;
    }

    /**
     * 分页插件,自动识别数据库类型
     */
    private PaginationInnerInterceptor paginationInnerInterceptor() {
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInnerInterceptor.setMaxLimit(-1L);
        // 溢出页面返回第一页
        paginationInnerInterceptor.setOverflow(true);
        return paginationInnerInterceptor;
    }

    /**
     * 乐观锁插件
     */
    private OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
        return new OptimisticLockerInnerInterceptor();
    }

    /**
     * 阻断插件(防删库跑路插件.如果是对全表的删除或更新操作,就会终止该操作)
     */
    private BlockAttackInnerInterceptor blockAttackInnerInterceptor() {
        return new BlockAttackInnerInterceptor();
    }
}

对于MP的默认设置,不太需要开发人员进行配置。这里的默认配置是应该作为一个属性源,直接加载到 springboot 中的,但是 springboot 默认只支持 Properties 的属性源,所以这里需要一个能支持 yaml 格式的属性源工厂 YmlPropertySourceFactory 替换掉默认属性源工厂。

MP的默认设置,这样需要在模块中加入一个 default-mybatis-plus.yaml 的配置文件。

5.2 yaml 配置工厂

/**
 * yaml配置工厂.可以将yaml文件解析为Properties
 */
public class YmlPropertySourceFactory extends DefaultPropertySourceFactory  {
    /**
     * 把yaml文件转换为属性源
     *
     * @param name 属性源的名称
     * @param resource 资源
     * @return 属性源
     * @throws IOException IO异常
     */
    @NonNull
    @Override
    public PropertySource<?> createPropertySource(@Nullable String name, @NonNull EncodedResource resource) throws IOException {
        String sourceName = resource.getResource().getFilename();
        if (StrUtil.isNotBlank(sourceName) && StrUtil.endWithAny(sourceName, ".yml", ".yaml")) {
            YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
            factory.setResources(resource.getResource());
            factory.afterPropertiesSet();
            return new PropertiesPropertySource(sourceName, Objects.requireNonNull(factory.getObject()));
        }
        return super.createPropertySource(name, resource);
    }
}

5.3 resources/default-mybatis-plus.yaml

默认配置的 yaml 文件,如下代码所示:

# Mybatis-Plus 默认配置
# 更多介绍 : https://baomidou.com/
mybatis-plus:
  # 启动时是否检查 MyBatis XML 文件的存在,默认不检查
  checkConfigLocation: false
  configuration:
    # 自动驼峰命名映射
    mapUnderscoreToCamelCase: true
    # MyBatis 自动映射策略
    # NONE:不启用 PARTIAL:只对非嵌套 resultMap 自动映射 FULL:对所有 resultMap 自动映射
    autoMappingBehavior: FULL
    # MyBatis 自动映射时未知列或未知属性处理策
    # NONE:不做处理 WARNING:打印相关警告 FAILING:抛出异常和详细信息
    autoMappingUnknownColumnBehavior: NONE
    # 关闭日志记录 (使用p6spy进行日志输出)
    # 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
    logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
  global-config:
    # 是否打印 Logo banner
    banner: false
    dbConfig:
      # 主键类型
      # AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
      idType: ASSIGN_ID
      insertStrategy: NOT_NULL
      updateStrategy: NOT_NULL
      whereStrategy: NOT_NULL

6. 加解密策略枚举 DbSafeStrategy

package com.chinaums.security.strategy;
/**
 * 数据库加解密策略
 */
@AllArgsConstructor
public enum DbSafeStrategy {

    /**
     * SM2 加密算法
     * 可通过EncryptUtil.generateSm2Key()方法自行生成公钥和私钥
     */
    ENCRYPT_SM2(s -> EncryptUtil.encryptBySm2(s, "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEik1NRz0PJ4xe+puBGfeCn44lNyjUyaNkza/wIdwBT0fjywVqauLeav3PSLF0/pKmTedrR6QJD/3w62yCYS1OTQ=="),
            s -> EncryptUtil.decryptBySm2(s, "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgnCoYlS6/lCPDfWjNJyQozC0EkdKYorTCOb+xC4tsatugCgYIKoEcz1UBgi2hRANCAASKTU1HPQ8njF76m4EZ94KfjiU3KNTJo2TNr/Ah3AFPR+PLBWpq4t5q/c9IsXT+kqZN52tHpAkP/fDrbIJhLU5N")),

    /**
     * SM3 加密算法
     */
    ENCRYPT_SM3(EncryptUtil::encryptBySm3, s -> s),


    /**
     * SM4 加密算法
     * 秘钥字符串为16位长度任意英文字符串,可通过RandomUtil.randomString(16)方法生成
     */
    ENCRYPT_SM4(s -> EncryptUtil.encryptBySm4(s, "ogb1iu59rfsk2y75"),
            s -> EncryptUtil.decryptBySm4(s, "ogb1iu59rfsk2y75")),

    /**
     * AES 加密算法
     * 秘钥字符串为16位、24位、32位长度任意英文字符串,可通过RandomUtil.randomString(32)方法生成
     */
    ENCRYPT_AES(s -> EncryptUtil.encryptByAes(s, "kj6j1vgq5nj3biph42jnqn2yre70accm"),
            s -> EncryptUtil.decryptByAes(s, "kj6j1vgq5nj3biph42jnqn2yre70accm")),

    /**
     * RSA 加密算法
     * 可通过EncryptUtil.generateRsaKey()方法自行生成公钥和私钥
     */
    ENCRYPT_RSA(s -> EncryptUtil.encryptByRsa(s, "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0ovKFQIwWK+ueJUxN1SSQKrlbegKQ7r9msu8Ar4hVD5f6cG/qZ4iv5QveVeiUAg499CDa1448aJXi2eqkmODeM6Si65BHdPH1urmL4O1uEysaIKquztiPsVix9E8azmK08balqXYbE7ybBQSIQ+yEXUJ+3M+WwmxK2lxZica8ywIDAQAB"),
            s -> EncryptUtil.decryptByRsa(s, "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALSi8oVAjBYr654lTE3VJJAquVt6ApDuv2ay7wCviFUPl/pwb+pniK/lC95V6JQCDj30INrXjjxoleLZ6qSY4N4zpKLrkEd08fW6uYvg7W4TKxogqq7O2I+xWLH0TxrOYrTxtqWpdhsTvJsFBIhD7IRdQn7cz5bCbEraXFmJxrzLAgMBAAECgYA7tiG1KsEkEyCwBmRS1kJf5b+gHZT7k/BxYnTfJSdL9vumLcTRF6h3fJ+Pv5ZCVuueTzUNInRCQ9BITQDjqCWsvnbDXQ3QHHwNy1ehK/DPgRbTYA9gvxLoLt6xRYuvPTPWFRwJNQCCIpsOKu/efQjb6xDMRUWKiXmBwt1nObnHAQJBAPjFFwcRcI1aWSXNrm+B4oHjKqJmHBeZ/inHLhOoheHTtqTMcjb1Q8jyurEjkFNWScIVOnBe1QKdpZJQaoY9FlUCQQC54u2Yqit8b2hPqGiOUwUd37ScfIcza3syyLjEU4YSVgtt+hpJ7Xi4e2dAne0zDW0Kk9hkqs2sGKDJYUYXMGafAkEA1uc0AGghagsdrhmj0jJLIVfEEezR4dWnCiJF/Ld9iNujEXSISk/QkfyWKMaHPGbzatV52W8i5pKXYPFVRMfqzQJAKVe/YGT4pwRgPtdF6eGtEaffk65eo6EUFYdvELtC5nEcuakWj7qxTtajcEuvpdsmlWOsjTcv50bS+/cWj7HEIQJAN2aZOY2v0/jymrObUcsH+d9VPQFd15CSdjJCTJ764aupj0tslsnm36wQq5eDn9PxmzGWOiwZYDBXIXVF3Fq9Eg==")),

    /**
     * SHA256 加密算法
     */
    ENCRYPT_SHA256(EncryptUtil::encryptBySha256, s -> s),

    /**
     * MD5 加密算法
     */
    ENCRYPT_MD5(EncryptUtil::encryptByMd5, s -> s);

    /**
     * 加密器
     */
    private final Function<String, String> encryptor;

    /**
     * 解密器
     */
    private final Function<String, String> decryptor;

    /**
     * 获得具体的脱敏器
     *
     * @return 具体的脱敏器
     */
    public Function<String, String> encryptor() {
        return encryptor;
    }

    public Function<String, String> decryptor() {
        return decryptor;
    }
}

通过枚举类的方式实现了加解密策略,最核心的思想就是函数式编程。这个 Function,它是 java.util.function 包下的。

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
    //......
}

这种函数式编程,最大的好处是:指定一个方法,去处理字符串,而具体怎么处理可以自己定义。比如我这里想定义 SM4 加解密算法。

ENCRYPT_SM4(s -> EncryptUtil.encryptBySm4(s, "ogb1iu59rfsk2y75"),
            s -> EncryptUtil.decryptBySm4(s, "ogb1iu59rfsk2y75")),

调用:

(DbSafeStrategy 对象).decryptor().apply(value)

7. 加解密工具类

package com.chinaums.security.util;
/**
 * 常用加密工具类
 */
public class EncryptUtil {

    /**
     * 公钥
     */
    public static final String PUBLIC_KEY = "publicKey";
    /**
     * 私钥
     */
    public static final String PRIVATE_KEY = "privateKey";

    /**
     * Base64加密
     *
     * @param data 待加密数据
     * @return 加密后字符串
     */
    public static String encryptByBase64(String data) {
        return Base64.encode(data, StandardCharsets.UTF_8);
    }

    /**
     * Base64解密
     *
     * @param data 待解密数据
     * @return 解密后字符串
     */
    public static String decryptByBase64(String data) {
        return Base64.decodeStr(data, StandardCharsets.UTF_8);
    }

    /**
     * AES加密
     *
     * @param data     待解密数据
     * @param password 秘钥字符串
     * @return 加密后字符串, 采用Base64编码
     */
    public static String encryptByAes(String data, String password) {
        if (StrUtil.isBlank(password)) {
            throw new IllegalArgumentException("AES需要传入秘钥信息");
        }
        // aes算法的秘钥要求是16位、24位、32位
        int[] array = {16, 24, 32};
        if (!ArrayUtil.contains(array, password.length())) {
            throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
        }
        return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
    }

    /**
     * AES加密
     *
     * @param data     待解密数据
     * @param password 秘钥字符串
     * @return 加密后字符串, 采用Hex编码
     */
    public static String encryptByAesHex(String data, String password) {
        if (StrUtil.isBlank(password)) {
            throw new IllegalArgumentException("AES需要传入秘钥信息");
        }
        // aes算法的秘钥要求是16位、24位、32位
        int[] array = {16, 24, 32};
        if (!ArrayUtil.contains(array, password.length())) {
            throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
        }
        return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8);
    }

    /**
     * AES解密
     *
     * @param data     待解密数据
     * @param password 秘钥字符串
     * @return 解密后字符串
     */
    public static String decryptByAes(String data, String password) {
        if (StrUtil.isBlank(password)) {
            throw new IllegalArgumentException("AES需要传入秘钥信息");
        }
        // aes算法的秘钥要求是16位、24位、32位
        int[] array = {16, 24, 32};
        if (!ArrayUtil.contains(array, password.length())) {
            throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
        }
        return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
    }

    /**
     * sm4加密
     *
     * @param data     待加密数据
     * @param password 秘钥字符串
     * @return 加密后字符串, 采用Base64编码
     */
    public static String encryptBySm4(String data, String password) {
        if (StrUtil.isBlank(data)) {
            return data;
        }
        if (StrUtil.isBlank(password)) {
            throw new IllegalArgumentException("SM4需要传入秘钥信息");
        }
        // sm4算法的秘钥要求是16位长度
        int sm4PasswordLength = 16;
        if (sm4PasswordLength != password.length()) {
            throw new IllegalArgumentException("SM4秘钥长度要求为16位");
        }
        return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
    }

    /**
     * sm4加密
     *
     * @param data     待加密数据
     * @param password 秘钥字符串
     * @return 加密后字符串, 采用Base64编码
     */
    public static String encryptBySm4Hex(String data, String password) {
        if (StrUtil.isBlank(password)) {
            throw new IllegalArgumentException("SM4需要传入秘钥信息");
        }
        // sm4算法的秘钥要求是16位长度
        int sm4PasswordLength = 16;
        if (sm4PasswordLength != password.length()) {
            throw new IllegalArgumentException("SM4秘钥长度要求为16位");
        }
        return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8);
    }

    /**
     * sm4解密
     *
     * @param data     待解密数据
     * @param password 秘钥字符串
     * @return 解密后字符串
     */
    public static String decryptBySm4(String data, String password) {
        if (StrUtil.isBlank(data)) {return data;}
        if (StrUtil.isBlank(password)) {
            throw new IllegalArgumentException("SM4需要传入秘钥信息");
        }
        // sm4算法的秘钥要求是16位长度
        int sm4PasswordLength = 16;
        if (sm4PasswordLength != password.length()) {
            throw new IllegalArgumentException("SM4秘钥长度要求为16位");
        }
        return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
    }

    /**
     * 产生sm2加解密需要的公钥和私钥
     *
     * @return 公私钥Map
     */
    public static Map<String, String> generateSm2Key() {
        Map<String, String> keyMap = new HashMap<>(2);
        SM2 sm2 = SmUtil.sm2();
        keyMap.put(PRIVATE_KEY, sm2.getPrivateKeyBase64());
        keyMap.put(PUBLIC_KEY, sm2.getPublicKeyBase64());
        return keyMap;
    }

    /**
     * sm2公钥加密
     *
     * @param data      待加密数据
     * @param publicKey 公钥
     * @return 加密后字符串, 采用Base64编码
     */
    public static String encryptBySm2(String data, String publicKey) {
        if (StrUtil.isBlank(publicKey)) {
            throw new IllegalArgumentException("SM2需要传入公钥进行加密");
        }
        SM2 sm2 = SmUtil.sm2(null, publicKey);
        return sm2.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
    }

    /**
     * sm2公钥加密
     *
     * @param data      待加密数据
     * @param publicKey 公钥
     * @return 加密后字符串, 采用Hex编码
     */
    public static String encryptBySm2Hex(String data, String publicKey) {
        if (StrUtil.isBlank(publicKey)) {
            throw new IllegalArgumentException("SM2需要传入公钥进行加密");
        }
        SM2 sm2 = SmUtil.sm2(null, publicKey);
        return sm2.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey);
    }

    /**
     * sm2私钥解密
     *
     * @param data       待加密数据
     * @param privateKey 私钥
     * @return 解密后字符串
     */
    public static String decryptBySm2(String data, String privateKey) {
        if (StrUtil.isBlank(privateKey)) {
            throw new IllegalArgumentException("SM2需要传入私钥进行解密");
        }
        SM2 sm2 = SmUtil.sm2(privateKey, null);
        return sm2.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
    }

    /**
     * 产生RSA加解密需要的公钥和私钥
     *
     * @return 公私钥Map
     */
    public static Map<String, String> generateRsaKey() {
        Map<String, String> keyMap = new HashMap<>(2);
        RSA rsa = SecureUtil.rsa();
        keyMap.put(PRIVATE_KEY, rsa.getPrivateKeyBase64());
        keyMap.put(PUBLIC_KEY, rsa.getPublicKeyBase64());
        return keyMap;
    }

    /**
     * rsa公钥加密
     *
     * @param data      待加密数据
     * @param publicKey 公钥
     * @return 加密后字符串, 采用Base64编码
     */
    public static String encryptByRsa(String data, String publicKey) {
        if (StrUtil.isBlank(publicKey)) {
            throw new IllegalArgumentException("RSA需要传入公钥进行加密");
        }
        RSA rsa = SecureUtil.rsa(null, publicKey);
        return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
    }

    /**
     * rsa公钥加密
     *
     * @param data      待加密数据
     * @param publicKey 公钥
     * @return 加密后字符串, 采用Hex编码
     */
    public static String encryptByRsaHex(String data, String publicKey) {
        if (StrUtil.isBlank(publicKey)) {
            throw new IllegalArgumentException("RSA需要传入公钥进行加密");
        }
        RSA rsa = SecureUtil.rsa(null, publicKey);
        return rsa.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey);
    }

    /**
     * rsa私钥解密
     *
     * @param data       待加密数据
     * @param privateKey 私钥
     * @return 解密后字符串
     */
    public static String decryptByRsa(String data, String privateKey) {
        if (StrUtil.isBlank(privateKey)) {
            throw new IllegalArgumentException("RSA需要传入私钥进行解密");
        }
        RSA rsa = SecureUtil.rsa(privateKey, null);
        return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
    }

    /**
     * md5加密
     *
     * @param data 待加密数据
     * @return 加密后字符串, 采用Hex编码
     */
    public static String encryptByMd5(String data) {
        return SecureUtil.md5(data);
    }

    /**
     * sha256加密
     *
     * @param data 待加密数据
     * @return 加密后字符串, 采用Hex编码
     */
    public static String encryptBySha256(String data) {
        return SecureUtil.sha256(data);
    }

    /**
     * sm3加密
     *
     * @param data 待加密数据
     * @return 加密后字符串, 采用Hex编码
     */
    public static String encryptBySm3(String data) {
        return SmUtil.sm3(data);
    }

}

8. application.yml

#mybatis
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  #实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.ah.ums.service.**.entity
  global-config:
    #数据库相关配置
    db-config:
      #主键类型  AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
      id-type: INPUT
      #字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
      insert-strategy: NOT_NULL
      #字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
      update-strategy: NOT_NULL
      #驼峰下划线转换
      column-underline: true
      logic-delete-value: 1
      logic-not-delete-value: 0
    banner: false
  #原生配置
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
    call-setters-on-nulls: true
    jdbc-type-for-null: 'null'
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志

# 数据库字段加密
mybatis-extends:
  # 是否开启加密
  db-safe:
    enable: true

9. 使用

@Data
public class SysUserVo implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    private Long userId;
	
    /**
     * 用户账号
     */
    private String userNo;

    /**
     * 用户名称密文
     */
    @DbSafe
    private String userName;
}
### 使用 MyBatis Plus 创建自定义拦截器实现数据脱敏 #### 定义自定义注解 为了标记需要进行加密/解密处理的类和字段,创建两个自定义注解 `EncryptDecryptClass` 和 `EncryptDecryptField`。 ```java import java.lang.annotation.*; @Documented @Inherited @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptDecryptClass { } ``` ```java import java.lang.annotation.*; @Documented @Inherited @Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptDecryptField { } ``` 这些注解用于标注哪些实体类及其属性应该被加密或解密[^2]。 #### 编写拦截器逻辑 MyBatis允许开发者通过插件机制对SQL执行流程中的特定阶段进行干预。对于本案例而言,主要关注的是`ParameterHandler`(负责准备参数)以及`ResultSetHandler`(负责解析查询结果)。因此,在编写拦截器时需针对这两个组件做文章: - **ParameterHandler**:当向数据库发送请求之前,遍历所有待插入的数据项;如果发现带有指定注解,则触发相应的加解密算法。 - **ResultSetHandler**:从数据库获取记录之后再对其进行加工转换——即读取出来的敏感信息先经过逆运算还原成明文形式返回给业务层调用者。 下面是一个简单的例子展示如何构建这样的拦截器: ```java import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import java.sql.PreparedStatement; import java.util.Properties; @Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class), }) public class DataMaskingInterceptor implements Interceptor { private static final ThreadLocal<Boolean> IS_ENCRYPTED = new ThreadLocal<>(); @Override public Object intercept(Invocation invocation) throws Throwable { // 获取当前invocation对象所携带的实际参数列表 Object[] args = invocation.getArgs(); // 假设第一个参数总是PreparedStatement实例 PreparedStatement ps = (PreparedStatement)args[0]; MetaObject metaObj = SystemMetaObject.forObject(ps.getParameterMetaData()); Object parameterValue = ((ParameterHandler)invocation.getTarget()).getParameterObject(); processParams(parameterValue); return invocation.proceed(); } /** * 对象预处理器,递归地访问每一个成员变量并应用必要的变换. */ protected void processParams(Object param){ if(param instanceof Map){ ((Map<?, ?>)param).forEach((key,value)->{ if(value.getClass().isAnnotationPresent(EncryptDecryptClass.class)){ encryptOrDecryptFields(value); } }); }else if(param!=null && param.getClass().isAnnotationPresent(EncryptDecryptClass.class)){ encryptOrDecryptFields(param); } } /** * 加密或解密具有相应注解的字段. */ private void encryptOrDecryptFields(Object obj){ Field[] fields=obj.getClass().getDeclaredFields(); Arrays.stream(fields).filter(field->field.isAnnotationPresent(EncryptDecryptField.class)) .forEachOrdered(field -> { try { field.setAccessible(true); Object value=field.get(obj); // 这里简单模拟了加密操作,实际应替换为具体的加密库函数 String encryptedVal=(String)value+"*masked"; field.set(obj,encryptedVal); } catch (IllegalAccessException e) { throw new RuntimeException(e.getMessage(),e); } }); } @Override public Object plugin(Object target) { return Plugin.wrap(target,this); } @Override public void setProperties(Properties properties) {} } ``` 上述代码片段展示了如何利用MyBatis提供的API来捕捉到即将被执行的操作,并在此基础上加入额外的安全措施[^1]。 #### 应用场景配置 为了让这个新的拦截器生效,还需要将其注册至Spring Boot应用程序上下文中。通常做法是在启动类或者其他合适的地方添加如下配置: ```yaml mybatis-plus: global-config: db-config: id-type: auto configuration: map-underscore-to-camel-case: true cache-enabled: false type-enums-package: com.example.enums mapper-locations: classpath*:mapper/*.xml interceptors: - com.yourpackage.DataMaskingInterceptor ``` 另外,在DAO接口处也可以直接声明使用该特性: ```java @Mapper public interface UserMapper extends BaseMapper<User> { int insertSelective(@EncryptTransaction User record); } ``` 这样就完成了整个基于MyBatis Plus框架下的简易版数据脱敏方案的设计与实施[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会叫的狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值