文章目录
- Mybatis数据加密解密
- 一、自定义注解
- 二、自定义参数处理拦截器
- 结果集拦截器
- 加密解密
Mybatis数据加密解密
方案一:Mybatis拦截器之数据加密解密【Interceptor】
拦截器介绍
Mybatis Interceptor 在 Mybatis 中被当作 Plugin(插件),不知道为什么,但确实是在 org.apache.ibatis.plugin 包下面
既然是拦截器,可以拦截哪些内容呢?试想一下… 当程序写到持久层时,Mybatis 会 执行 指定 SQL 语句,并处理 请求参数 和 返回值。没错,Mybatis 拦截器可以帮助我们处理上述内容,请看官网的 Plugins 的片段, 内容不多
// 执行
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
// 请求参数处理
ParameterHandler (getParameterObject, setParameters)
// 返回结果集处理
ResultSetHandler (handleResultSets, handleOutputParameters)
// SQL语句构建
StatementHandler (prepare, parameterize, batch, update, query)
拦截器的使用
如果需要实现自定义的拦截器,只需要实现 org.apache.ibatis.plugin.Interceptor 接口,该接口有三个方法:
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
我们要实现数据加密,进入数据库的字段不能是真实的数据,但是返回来的数据要真实可用,所以我们需要针对 Parameter 和 ResultSet 两种类型处理,同时为了更灵活的使用,我们需要自定义注解
一、自定义注解
类注解,将注解放在实体类上
/**
* 需要加解密的类注解
*/
@Documented
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDecryptClass {
}
字段注解,将注解放在实体字段上
/**
* 加密字段注解
*/
@Documented
@Inherited
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDecryptField {
}
有了这两个注解,我们可以在我们可以标记我们要处理的实体和实体中的字段
二、自定义参数处理拦截器
参考官网,通过 @Intercepts 和 @Signature 的联合使用,指定 ParameterHandler.class 类型,同时通过 @Component 注解注入到容器中,即可在设置参数的时候进行拦截,通过自定义接口 IEncryptDecrypt, 根据 Field 的各种类型自定义加密解密算法
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
@ConditionalOnProperty(value = "domain.encrypt", havingValue = "true")
@Component
@Slf4j
public class ParammeterInterceptor implements Interceptor {
@Autowired
private IEncryptDecrypt encryptDecrypt;
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("拦截器ParamInterceptor");
//拦截 ParameterHandler 的 setParameters 方法 动态设置参数
if (invocation.getTarget() instanceof ParameterHandler) {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
// 反射获取 BoundSql 对象,此对象包含生成的sql和sql的参数map映射
/*Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql");
boundSqlField.setAccessible(true);
BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler);*/
// 反射获取 参数对像
Field parameterField =
parameterHandler.getClass().getDeclaredField("parameterObject");
parameterField.setAccessible(true);
Object parameterObject = parameterField.get(parameterHandler);
if (Objects.nonNull(parameterObject)){
Class<?> parameterObjectClass = parameterObject.getClass();
EncryptDecryptClass encryptDecryptClass = AnnotationUtils.findAnnotation(parameterObjectClass, EncryptDecryptClass.class);
if (Objects.nonNull(encryptDecryptClass)){
Field[] declaredFields = parameterObjectClass.getDeclaredFields();
final Object encrypt = encryptDecrypt.encrypt(declaredFields, parameterObject);
}
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
}
同样新建结果集拦截器
结果集拦截器
与参数拦截器基本一样, 只不过类型指定为 ResultSetHandler.class
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
})
@ConditionalOnProperty(value = "domain.decrypt", havingValue = "true")
@Component
@Slf4j
public class ResultInterceptor implements Interceptor {
@Autowired
private IEncryptDecrypt encryptDecrypt;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
if (Objects.isNull(result)){
return null;
}
if (result instanceof ArrayList) {
ArrayList resultList = (ArrayList) result;
if (CollectionUtils.isNotEmpty(resultList) && needToDecrypt(resultList.get(0))){
for (int i = 0; i < resultList.size(); i++) {
encryptDecrypt.decrypt(resultList.get(i));
}
}
}else {
if (needToDecrypt(result)){
encryptDecrypt.decrypt(result);
}
}
return result;
}
public boolean needToDecrypt(Object object){
Class<?> objectClass = object.getClass();
EncryptDecryptClass encryptDecryptClass = AnnotationUtils.findAnnotation(objectClass, EncryptDecryptClass.class);
if (Objects.nonNull(encryptDecryptClass)){
return true;
}
return false;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
加密解密
IEncryptDecrypt 接口定义了 加密和解密两个方法,
public interface IEncryptDecrypt {
/**
* 加密方法
* @param declaredFields 反射bean成员变量
* @param parameterObject Mybatis入参
* @param <T>
* @return
*/
public <T> T encrypt(Field[] declaredFields, T parameterObject) throws IllegalAccessException;
/**
* 解密方法
* @param result Mybatis 返回值,需要判断是否是ArrayList类型
* @param <T>
* @return
*/
public <T> T decrypt(T result) throws IllegalAccessException;
}
两个拦截器通过在 YAML 中配置属性,按条件注入,外加自定义加密解密算法,完成全局灵活的配置。
核心代码已上传至 Github Demo
方案二:Mybatis利用内置类型转换器【typeHandler】
mybatis 利用内置类型转换器(「typeHandler」),实现 Java 类型与 JDBC 类型的相互转换,我们正好可以利用这个特性,在转换之前加入加解密步骤。
typeHandler 底层原理不是复杂,如果我们没有使用 Mybatis,而是直接使用最原始的 JDBC 执行查询语句,相关代码如下:
我们需要手动判断 Java 类型,然后调用 PreparedStatement设置合适类型参数。获取返回结果之后,又需要手动调用 ResultSet 结果集获取相应类型的数据,这个过程十分繁琐。使用 mybatis 之后,上述步骤就无需我们再实现了。mybatis 可以通过识别 Java/JDBC 类型,调用相应typeHandler,自动实现转换逻辑。下图为 mybatis 内置类型转换器,基本涵盖了所有 「Java/JDBC」 数据类型。
通用解决方案
自定义 typeHandler
下面我们来实现带有加解密功能的类型转换器,实现方式也比较简单,只要继承 org.apache.ibatis.type.BaseTypeHandler,重写相关方法。
简单起见,上述加解密仅使用了 Base64,大家可以替换成相应加解密算法即或者引入相应加解密服务。
在这里插入图片描述
其中加密转换将在 setNonNullParameter 中执行,解密转换将在 getNullableResult中执行。CryptTypeHandler 使用一个 MappedTypes 注解,包含一个 CryptType 类,这个类使用 mybatis 别名功能,可以极大简化 sqlmap 相关配置。
注册 typeHandler
使用方必须将 typeHandler 和 alias 注册到 mybatis 中,否则无法生效。下面提供三种方式,可以根据项目情况选择其中一种即可:
「单独使用 mybatis」
这种场景需要在 「mybatis-config.xml」 配置,mybatis 启动时将会加载该配置文件。
<typeHandlers>
<!--类型转换器包路径-->
<package name="com.xx.xx"/>
</typeHandlers>
<!-- 别名定义 -->
<typeAliases>
<!-- 针对单个别名定义 type:类型的路径 alias:别名 -->
<typeAlias type="xx.xx.xx" alias="xx"/>
</typeAliases>
「使用 Spring 配置 Mybatis Bean」
配合 Spring 使用时需要将 typeHandler 注入 SqlSessionFactoryBean ,配置方式如下:
<!-- MyBatis 工厂 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!--alias 注入-->
<property name="typeAliasesPackage" value="xx.xx.xx"/>
<!-- typeHandlers 注入 -->
<property name="typeHandlersPackage" value="xx.xx.xx"/>
</bean>
「SpringBoot」
SpringBoot 方式就最简单了,只要引入 mybatis-starter,配置文件加入如下配置即可:
## mybatis 配置
# 类型转换器包路径
mybatis.type-handlers-package=com.xx.xx.x
mybatis.type-aliases-package=com.xx.xx
修改 mapper sql 配置
最后我们只要简单修改 mapper 中 resultMap 或 sql s配置就可以实现加解密。假设我们对现有一张 「bank_card」 表进行加解密,表结构如下:
CREATE TABLE bank_card (
id int primary key auto_increment,
gmt_create timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
gmt_update timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
card_no varchar(256) NOT NULL DEFAULT '' COMMENT '卡号',
phone varchar(256) NOT NULL DEFAULT '' COMMENT '手机号',
name varchar(256) NOT NULL DEFAULT '' COMMENT '姓名',
id_no varchar(256) NOT NULL DEFAULT '' COMMENT '证件号'
);
「insert 加密」
现需要对 card_no,phone,name,id_no 进行加密,「insert」 语句加密示例:
<insert id="insertBankCard" keyProperty="id" useGeneratedKeys="true" parameterType="org.demo.pojo.BankCardDO">
INSERT INTO bank_card (card_no, phone,name,id_no)
VALUES
(#{card_no,javaType=crypt},
#{phone,typeHandler=org.demo.type.CryptTypeHandler},
#{name,javaType=crypt},
#{id_no,javaType=crypt})
</insert>
我们只需要在 「#{}」 指定 typeHandler,传入参数最后将被加密。使用 typeHandler需要使用类的全路径,比较繁琐,我们可以使用 「javaType」 属性,直接使用上面我们的定义别名 「crypt」。数据库最终执行sql 如下:
INSERT INTO bank_card (card_no, phone,name,id_no) VALUES ('NjQzMjEyMzEyMzE=', 'MTM1Njc4OTEyMzQ=', '5rWL6K+V5Y2h', 'MTIzMTIzMTIzMQ==');
推荐一款 IDEA 的插件 「mybatis-log-plugin」,可以自动将 mybatis sql 日志还原成真实执行 sql
「查询加解密」普通查询解密示例如下:
<resultMap id="bankCardXml" type="org.demo.pojo.BankCardDO">
<result property="card_no" column="card_no" typeHandler="org.demo.type.CryptTypeHandler"/>
<result property="name" column="name" typeHandler="org.demo.type.CryptTypeHandler"/>
<result property="id_no" column="id_no" typeHandler="org.demo.type.CryptTypeHandler"/>
<result property="phone" column="phone" typeHandler="org.demo.type.CryptTypeHandler"/>
</resultMap>
<select id="queryById" resultMap="bankCardXml">
select * from bank_card where id=#{id}
</select>
这里我们在 「select」 配置中只能使用 resultMap 属性,指定 typeHandler 。数据库明文、密文共存的情况,查询解密示例如下:
<!-- resultMap 同上 -->
<select id="queryByPhone" resultMap="bankCardXml">
select * from bank_card where phone in(#{card_no,javaType=crypt},#{card_no})
</select>
最后我们可以将自定义的 typeHandler 单独打包发布,其他业务方只需要引用,改造相关配置文件,即可完成数据加解密。上述代码示例已上传至 Github
总结
借助于自定义的 typeHandler,我们实现了一个通用的加解密的方案,该方案对于使用方来说代码侵入性小,开箱即用,可以快速完成加解密的改造。