采用mybatis插件,实现数据入库加密,出库解密

前言:

  在大部分的系统中,一方面出于用户的隐私安全考虑,让开发和业务数据无感知,都会对数据库内容进行加密,那么在书写逻辑时加密也不太现实。复杂的实现也不考虑,本文将采用mybatis的拦截器作为基础进行实现,也算是对工作中这段时间的实践进行总结与思考,避坑。


实现思路:

  • 入库时,切入点为mapper方法的参数 ,对其中标注了加密注解的值进行加密
  • 出库时,切入点为mapper方法的返回值,其中标注了加密注解的返回值,进行解密

采用mybatis插件,实现数据入库加密,出库解密_第1张图片
其中加密和解密作为可选的选项可单向配置。
  但一般情况下,都是在实体类中注解参数,实现入库加密出库解密的操作,也支持在各种参数前注解。
  使用代码定义切入点以及目标就变得尤为重要,这里使用注解配合mybatis的拦截器,对数据库数据的进出实现拦截,(拦截Executor的三个重载方法)

拦截器的范围以及标识解释:

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback,
    getTransaction, close, isClosed) --执行sql
  • ParameterHandler (getParameterObject, setParameters)
    –获取、设置参数
  • ResultSetHandler (handleResultSets, handleOutputParameters)
    –处理结果集
  • StatementHandler (prepare, parameterize, batch, update, query)
    –记录sql

  这里需要注意的是,这4个类型是固定的,里面的方法也是固定的,不能再被改变的,具体的信息,可以进入相关的类(Executor.class、ParameterHandler .class、ResultSetHandler .class、StatementHandler .class)查看。


插件实现方式:

  Mybatis开放的接口Interceptor ,可以让开发者自己实现自定义的拦截器,只需要实现这个接口,并在mybatis.xml中配置,即可生效,如果是springboot,可用java类的形式注册。

方法解释:

  • intercept方法:
      插件拦截到的对象主要的执行方法

  • plugin方法
      为目标对象生成一个代理对象

  • setProperties方法
      可以为插件的变量设置属性

jar包依赖:

已上传至maven官方中央仓库,在pom.xml文件写入以下坐标即可:

 
      com.github.kamjin1996
      mybatis-intercept-crypt
      2.0
 

下面内容篇幅较长,点击跳过快速开始


已填的坑(醒目):

问题一:

  在本地开发时可以加密 一切正常,在linux上却不行?

原因:
  在本地一切正常,在linux服务器上却无法加密,第一时间想到环境问题,但能有什么环境问题呢,由于加密或解密失败会try-catch掉并回滚为原来的值,也没加入过多的日志(因为需要频繁打印,有积少成多的性能损耗),所以未打印真实异常,便把加密代码写成了helloworld,独立放到服务器上,报了个
java.security.InvalidKeyException: Illegal key size

  这个原因主要是某些国家的进口管制限制,JDK默认的加解密有一定的限制,从Java 1.8.0_151和1.8.0_152开始,为JVM启用 无限制强度管辖策略 有了一种新的更简单的方法。如果不启用此功能,则不能使用AES-256。找到了这个问题所在,那么就有对应的解决方案

解决:
  两种解决方案:
    1、升级jdk
    2、修改jdk的参数

显然第二种更方便代价也小

在 jre/lib/security 文件夹中查找文件 java.security。
例如,对于Java 1.8.0_152,文件结构如下所示:
/jdk1.8.0_152
 |- /jre
  |- /lib
        |- /security
              |- java.security
现在用文本编辑器打开java.security,并找到定义java安全性属性crypto.policy的行,它可以有两个值limited或unlimited - 默认值是limited。
默认情况下,应该能找到一条注释掉的行:
#crypto.policy=unlimited

可以通过取消注释该行来启用无限制,删除#:
crypto.policy=unlimited

现在重新启动指向JVM的Java应用程序即可。

说了大白话就是去jdk目录下的jre/lib/security,找到java.security,去掉#crypto.policy=unlimited
的#号
然后kill掉java程序进程重新启动

问题二:

   mybatis的selectKey标签反回id的策略失效,导致insert无法拿到插入反回的主键id

原因:
  在对实体类字段做加解密时,为防止重复加密,即让其他下面代码还是使用未加密的值,所以对于实体类都进行了克隆,克隆后原对象不在引用,而新的对象赋值为加密的值,传入mybatis进行操作了,导致selectKey无法赋值给原对象并返回。(克隆出来的对象并没有进行返回)

解决:
  为了既能防止重复加密,又能使selectKey返回的id赋值给原对象,这里采用了一个hashMap来存储原对象与克隆对象,在插件运行结束前,从克隆对象中获取ID字段的值,赋值给原对象id字段,这样就解决了这个问题。

引用部分代码实现:
1、定义了一个map,用来存储原对象的引用和克隆对象的引用:

/** 存储源对象和新对象 */
	public static final ConcurrentHashMap<Object, Object> OLD_AND_NEW_OBJ_MAP =
        new ConcurrentHashMap<>();

2、在操作bean的处理器中,克隆的同时,对原对象和新对象以键值对形式存放。

// 对bean的所有操作,会影响本地数据,可能存在重复加密的情况,
// 需要clone成新bean,必须要有默认构造器
result = CryptInterceptor.OLD_AND_NEW_OBJ_MAP.computeIfAbsent(bean, BeanCryptHandler::clone);

3、存放后,任由程序继续执行,直到要退出插件,对id进行赋值,
这里由于扩大了excutor的拦截范围,两个query一个update方法,这样会使mapper方法对应的xml中的多条sql或其他不相干操作(多条sql是类似selectKey,不相干操作比如count)都走一遍插件,所以需要判断当前这次插件执行,是否可以进行对象池清理,判断的依据有两个:

  • 当次运行的statement的id获取到的method为空时,说明在做非本次sql查询的其他操作,需要跳过。
  • 当次只有在成功赋值id时,才进行清理。

代码实现:

private static void returnIdToSourceBean() {
        if (!OLD_AND_NEW_OBJ_MAP.isEmpty()) {
            try {
                Iterator<Map.Entry<Object, Object>> iterator = OLD_AND_NEW_OBJ_MAP.entrySet().iterator();
                boolean isDeal = Boolean.FALSE;
                while (iterator.hasNext()) {
                    Map.Entry<Object, Object> next = iterator.next();
                    Object sourceObj = next.getKey();
                    Object cloneObj = next.getValue();

                    Field sourceObjFieldId = sourceObj.getClass().getDeclaredField(TARGET_FIELD_ID);
                    if (sourceObjFieldId != null) {
                        sourceObjFieldId.setAccessible(Boolean.TRUE);
                        Field cloneObjFieldId = cloneObj.getClass().getDeclaredField(TARGET_FIELD_ID);
                        cloneObjFieldId.setAccessible(Boolean.TRUE);
                        Object cloneObjFieldIdVal = cloneObjFieldId.get(cloneObj);
                        if (Objects.nonNull(cloneObjFieldIdVal)) {
                            isDeal = Boolean.TRUE;
                            sourceObjFieldId.set(sourceObj, cloneObjFieldIdVal);
                        }
                    }
                }
                if (isDeal) {
                    clearObjMap();
                }
            } catch (Exception e) {
                log.error("fix bean id the method running failed.", e);
            }
        }
    }

    private static void clearObjMap() {
        OLD_AND_NEW_OBJ_MAP.clear();
    }

至此,表面问题基本解决完毕!


------------------------------------------9月12日更新------------------------------------

使用java类的形式注册所需配置类:

@Configuration
@Data
public class MybatisConfig {

    @Value("${dbcrypt.secretkey}")
    private String secretkey;

    @Value("${dbcrypt.enable}")
    private boolean enable;

    @Bean
    public CryptInterceptor cryptInterceptor() {
        return new CryptInterceptor();
    }

    @Bean
    public Dbcrypt dbcrypt() {
        return new Dbcrypt(AesEnum.AES192, getSecretkey(), isEnable());
    }
}

  虽然读取配置方式有很多种,但我们还是选择最简洁最好用的来用 ,配置的方式也更加灵活


快速开始:

  • 详细内容因为篇幅原因,请看这里:
    https://github.com/kamjin1996/mybatis-intercept-crypt

  • 口述不如实战,快速开始demo地址:
    https://github.com/kamjin1996/cryptdemo

小结:

  经过此次需求踩了不少的大坑小坑,学到了很多,灵活运用反射、aop、注解,可以让编码更舒适;

你可能感兴趣的:(JavaSE,Mysql)