在大部分的系统中,一方面出于用户的隐私安全考虑,让开发和业务数据无感知,都会对数据库内容进行加密,那么在书写逻辑时加密也不太现实。复杂的实现也不考虑,本文将采用mybatis的拦截器作为基础进行实现,也算是对工作中这段时间的实践进行总结与思考,避坑。
其中加密和解密作为可选的选项可单向配置。
但一般情况下,都是在实体类中注解参数,实现入库加密出库解密的操作,也支持在各种参数前注解。
使用代码定义切入点以及目标就变得尤为重要,这里使用注解配合mybatis的拦截器,对数据库数据的进出实现拦截,(拦截Executor的三个重载方法)
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
这里需要注意的是,这4个类型是固定的,里面的方法也是固定的,不能再被改变的,具体的信息,可以进入相关的类(Executor.class、ParameterHandler .class、ResultSetHandler .class、StatementHandler .class)查看。
Mybatis开放的接口Interceptor ,可以让开发者自己实现自定义的拦截器,只需要实现这个接口,并在mybatis.xml中配置,即可生效,如果是springboot,可用java类的形式注册。
方法解释:
intercept方法:
插件拦截到的对象主要的执行方法
plugin方法
为目标对象生成一个代理对象
setProperties方法
可以为插件的变量设置属性
已上传至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)都走一遍插件,所以需要判断当前这次插件执行,是否可以进行对象池清理,判断的依据有两个:
代码实现:
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、注解,可以让编码更舒适;