数据库字段加解密插件,保障数据的安全,支持Mybatis框架与MybatisPlus框架,数据入库加密,出库解密

7 篇文章 0 订阅
2 篇文章 0 订阅

前言:

  在很多的后台系统中,出于对用户隐私保护、数据安全、使开发和业务数据无感知、安全等级评审等需求中,都会要求对数据库部分内容进行加密,基于此种需求,在书写逻辑时直接调用工具类加密,查询出来结果后再解密,虽然简单粗暴但有如下缺点:

  • 代码量升高而加重代码复杂度,功能代码与业务代码耦合,侵入性过强
  • 疲于应付多个字段以及多种入参方式,要写更多的解析替换字段属性代码
  • 研发人员的关注点变多,关注点过多那么也意味着后续可能维护困难
  • 不够灵活且无扩展性
  • 做法不够优雅

基于以上需求及手动加解密的缺点,我们期待最好有一种声明式做法,比如注解,再配合AOP的思想,在最小影响原来代码的情况下,优雅的实现这个功能。
实现的方式将在下文有所罗列,以及相关可能遇到的问题也有所列出。本插件可以解决上述需求,或者为你提供一个思路,有一些项目我已经在使用此插件,目前情况稳定,你可以尝试在项目中使用此插件


一、 实现思路

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

简单图

思路很简单,切入点就是我们的Mapper方法,那么怎么拿到这个切入点呢?

可行的方式:

  1. 创建"公共mapper接口",所有的mapper都实现于此接口,此时只需要动态代理该接口对象,mapper中所有方法执行前检查随之加密参数,方法执行完成后解密返回值内参数(如果有的话)即可
  2. 利用springAOP,AspectJ切面的方式对方法进行拦截,增强等操作
  3. 利用DAO框架自带的拦截器、filter等。比如mybatis拦截器

方法1有一个很明显的问题是,我们常规的DAO框架中的mapper,一般也是被代理执行的,比如mybatis的mapper接口,mapper无需编写具体实现类,框架会结合mapper.xml动态的生成mapper对应的impl,或通过spring-mybatis的MapperFactoryBean
方法2的问题也类似,DAO框架其本身已经对mapper接口做过代理了,我们就不要再多此一举了
综合下来方法3是较确切可行的方式,而且mybatis的拦截器已经非常成熟了,无须担心其会发生意料之外的问题

———基于mybatis拦截器,再配合我们的自定义注解即可实现我们的需求。

二、准备

1.前提:

  1. 要支持的参数形式:实体类型参数中字段、String基础类型参数、Array,集合类型参数(元素为前两种类型)
  2. 不能影响原先的代码编写方式,且对实体类型对象中的值加密后,下面读取该值不能改变
  3. 要确保在加入此插件后,系统中其他功能不受影响,正常使用
  4. 配置简单、使用方便

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

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)查看。


三、插件实现

1).插件实现时的部分细节:

  • 加密/解密在注解上可以单向配置,使参数只加密或只解密(目前没体会到其意义,但支持)
  • 为了支持前提的第2条,则对实体类bean进行了深度copy,insert并返回主键采用了反射对克隆对象进行id赋值
  • 当使用mybatisplus时,一般使用者会参考官方demo注册一个分页插件:paginationInterceptor,此分页插件在执行时内部会判断IPage接口的isSearchCount参数,IPage的默认实现Page对象,其searchCount默认为true,为true时将会执行一条countSql优化,可以理解为执行一次主表的count(1)查询,此countSql将会导致后续的拦截器拦截到该sql,进而多执行相关的拦截操作。当前插件是对mapper的入参出参做处理,而在处理正常业务sql之前,countSql已经触发了对实体bean中字段的加密,后续紧随的处理正常业务的sql入参则是未加密的,导致出现total有数据,records却没有数据的问题。当前插件中对该默认值searchCount=false,以确保发生不必要的一系列问题,于是乎上述前提的第三条只能算是努力做到,没法完全做到,毕竟所有拦截器都是通过sql的执行来进行拦截的。且很难对其进行识别判断(不知该countSql是用户实际查询还是分页插件查询)
    这里补充说一下,一般分页查询,如果有查询总量的需求,那么提前查一次放在缓存即可,不必每次都countSql查询,实质上是否影响性能就见仁见智了。当然也不否认mybatisplus这种做法,很方便,但的确会让用户使用其他拦截器时造成一些困扰。

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

方法解释:

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

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

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

插件通过对mapper参数的拦截,取自定义注解标记的值进行加密,返回值做解密(如果有标记)

因篇幅问题,此处不展开代码内容,加解密插件源码已在Github开源,按照说明的方式引入,支持java,kotlin框架支持为:mybatis,mybatis-plus。文末包含源码地址

提示:问题篇幅较长,可以大致浏览一下,或者跳过,直达引入方式及使用

四、已填的坑(醒目)

问题一:
  在本地开发时可以加密 一切正常,在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赋值给原对象,这里采用threadLocal,在插件运行结束前,从克隆对象中获取ID字段的值,赋值给原对象id字段,这样就解决了这个问题。

引用部分代码实现:
1).用来存储原对象的引用和克隆对象的引用:

 /**
     * 线程副本存储ID 用以克隆的对象的ID赋值
     */
    private static final ThreadLocal<KeyGenerateReference> KEY_GENERATE_REFERENCE_THREAD_LOCAL = new ThreadLocal<>();

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

		//对bean的所有操作,会影响本地数据,可能存在重复加密的情况, 需要clone成新bean,必须要有默认构造器
        //属性的二次操作 通过线程副本来解决 比如id的返回 此处需要将result和bean的映射放入线程副本
        Object result = clone(bean);
        CryptHelper.setKeyGenerateReference(bean, result);

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

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

代码实现:

/**
     * 修复selectKey无法赋值给源对象(源对象被clone,因为需要避免重复加密)
     *
     * @param reference keyGen的引用
     * @throws IllegalAccessException 权限异常
     * @throws NoSuchFieldException   没有对应属性异常
     */
    private void returnIdToSourceBean(KeyGenerateReference reference) throws IllegalAccessException, NoSuchFieldException {
        if (reference != null) {
            Object sourceObj = reference.getOriginPojo();
            Object cloneObj = reference.getClonePojo();

            Field sourceObjFieldId = sourceObj.getClass().getDeclaredField(primaryKeyName);
            sourceObjFieldId.setAccessible(true);
            Field cloneObjFieldId = cloneObj.getClass().getDeclaredField(primaryKeyName);
            cloneObjFieldId.setAccessible(true);
            Object cloneObjFieldIdVal = cloneObjFieldId.get(cloneObj);
            if (Objects.nonNull(cloneObjFieldIdVal)) {
                sourceObjFieldId.set(sourceObj, cloneObjFieldIdVal);
            }
        }
    }

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

五、引入方式

1).依赖:
maven:
基于mybatis-plus-boot-stater:

 <dependency>
     <groupId>com.github.kamjin1996</groupId>
     <artifactId>kamjin-db-crypt-mybatis-plus-boot-starter</artifactId>
     <version>${last.version}</version>
 </dependency>

基于mybatis-spring:

<dependency>
     <groupId>com.github.kamjin1996</groupId>
     <artifactId>kamjin-db-crypt-mybatis</artifactId>
     <version>${last.version}</version>
 </dependency>

其他方式,以及版本等可查看:依赖坐标信息及版本
例如gradle:

implementation 'com.github.kamjin1996:kamjin-db-crypt-mybatis-plus-boot-starter:${last.version}'

2).配置

  • starter方式(推荐):
    可以不做配置,默认取下面的参数
   kamjin:
       dbcrypt:
        secretkey: 123456789012345678901234
        enable: true
        primary-key-name: id
        aes: AES192

配置释义:
在这里插入图片描述

  • 非starter方式:
    1). application.yaml: 同上starter的配置,PS:此方式关于参数的key可以自定义,只要确保能被spring解析到即可

    2). 新建配置类:

@Configuration
@Data
public class KamjinDbCryptConfig {

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

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

  @Value("${kamjin.dbcrypt.primary-key-name}")
  private String primaryKeyName;

  @Value("${kamjin.dbcrypt.aes}")
  private AesEnum aes;

  @Bean
  public MybatisCryptInterceptor mybatisCryptInterceptor() {
      DbcryptProperties properties = new DbcryptProperties(aes, secretkey, enable, primaryKeyName);
      CryptExecutorFactory.registry(new DefaultCryptExecutor(new DefaultAESCodecFieldValueHandler(properties)));
      return new MybatisCryptInterceptor(properties);
  }
}

注意:如果需要用javaBeanConfig配置MybatisPlusCryptInterceptor对象等相关配置,还需要@bean创建MybatisInterceptorInspectByAppRefreshedListener到spring容器中,参考的插件源码中的MybatisPlusCryptAutoConfiguration.java
手动配置完毕。

此外,项目如果较久远,采用mybatis.xml注册插件:

   <plugins>
        <plugin interceptor="com.kamjin.toolkit.db.crypt.mybatisplus.interceptor.MybatisPlusCryptInterceptor"/>
    </plugins>

其他bean定义在xml中由spring维护即可


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

3).使用
类字段注解@CryptField,也支持在mapper入参前注解@CryptField
常见用法,pojo的字段上注解

字段上使用:

@CryptField 
private String password;

mapper入参使用:

User selectByMobile(@CryptField @Param("mobile") String mobile);

更多使用方式参考下述demo



六、快速开始

欢迎提出你的意见以及建议,或者其他功能,改进,我将考虑并支持,感谢~


最近考虑在某个空闲时间,能采用kotlin重构该插件的代码及调整项目结构,目前仍然觉得插件虽够用,但代码质量及扩展性仍然没有那么出色,对现在的分层及依赖等不太满意,后面再说吧,欢迎在评论区留言讨论~~ 再会

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值