- 需求:在实际项目开发中,有许多的敏感信息,例如用户的手机号,身份证等。我们不希望这些数据以明文的方式展示在数据库中,降低安全风险。所以,引申出了敏感信息的加密问题
- 那么,对于敏感信息的加密,普遍的做法为,在insert的时候,手动在代码中,对这些敏感数据进行加密,之后再执行insert操作,查询时,将这些信息进行解密,之后返回给前端,进行数据的展示。
- 但是这种做法有个非常不好的弊端,就是,但凡包含这些敏感信息的操作,都需要在代码中,手动添加这些加密/解密的算法,导致代码的膨胀。那么,有没有一种更好的方法,能够实现这些功能呢?
- 相信大家都会很自然的想到,aop,拦截器?没错,在mybatis中,利用plugin插件式开发,可以在mybatis执行时,先被我们自定义的interceptor进行拦截,执行完interceptor方法之后,再执行真正的数据库操作。那么,下面看看mybatis plugin 如何进行使用来完成此项需求
- 到此,mybatis plugin 进行加密就已经完成,下面看看 查询时,通过 mybatis plugin进行解密
需求:在实际项目开发中,有许多的敏感信息,例如用户的手机号,身份证等。我们不希望这些数据以明文的方式展示在数据库中,降低安全风险。所以,引申出了敏感信息的加密问题
那么,对于敏感信息的加密,普遍的做法为,在insert的时候,手动在代码中,对这些敏感数据进行加密,之后再执行insert操作,查询时,将这些信息进行解密,之后返回给前端,进行数据的展示。
但是这种做法有个非常不好的弊端,就是,但凡包含这些敏感信息的操作,都需要在代码中,手动添加这些加密/解密的算法,导致代码的膨胀。那么,有没有一种更好的方法,能够实现这些功能呢?
相信大家都会很自然的想到,aop,拦截器?没错,在mybatis中,利用plugin插件式开发,可以在mybatis执行时,先被我们自定义的interceptor进行拦截,执行完interceptor方法之后,再执行真正的数据库操作。那么,下面看看mybatis plugin 如何进行使用来完成此项需求
首先呢,要让需要集成mybatis中的Interceptor接口,其定义了拦截器的规范,其中的intercept(Invocation invocation) 方法中,就是自定义的拦截逻辑
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
其次呢,需要在类上添加两个注解
其中的@Component 是 spring的注解,用于把这个类交给spring管理,这样mybatis 与spring整合的时候,就能用到这个组件。
然后是 @Intercepts() 注解,这个注解是 mybatis的,其中可以定义多个 @Signature(),用于拦截多种类型的sql语句
type为其拦截的类型,method 为其拦截的方法,args为这个拦截方法的传参类型。
其中type有四个类型可以传递
Executor, 执行器,其method可填方法如下
int update(MappedStatement ms, Object parameter) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
List<BatchResult> flushStatements() throws SQLException;
void commit(boolean required) throws SQLException;
void rollback(boolean required) throws SQLException;
Transaction getTransaction();
void close(boolean forceRollback);
boolean isClosed();
ParameterHandler, 参数处理器,其method可填方法如下
Object getParameterObject();
void setParameters(PreparedStatement var1) throws SQLException;
ResultSetHandler, 返回结果集处理器,其method可填方法如下
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
void handleOutputParameters(CallableStatement cs) throws SQLException;
StatementHandler,SQL语句处理器,其method可填方法如下
Statement prepare(Connection var1, Integer var2) throws SQLException;
void parameterize(Statement var1) throws SQLException;
void batch(Statement var1) throws SQLException;
int update(Statement var1) throws SQLException;
<E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;
Intercept拦截器代码,其中有三个方法 intercept(),plugin(),setProperties()
其中,intercept()是主要方法,其他先不看
@Intercepts({
//执行参数接口,method为接口名称,args为参数对象
@Signature(type= Executor.class, method="update", args={MappedStatement.class, Object.class})
})
@Component
public class InsertInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
}
@Override
public Object plugin(Object target) {
}
@Override
public void setProperties(Properties properties) {
}
}
Intercept() 方法逻辑代码
通过invocation.getArgs() 获取拦截器的参数
- 其中[0] 第一个参数,根据它判断当前的操作类型。
- 如果是insert or update,说明是新增操作,那么就需要对敏感信息进行一些的处理
首先可以通过 ms.getParameterMap(); 获取到这个执行的传入参数,对应于 xml文件中的 parameterType
<insert id="insertUser" parameterType="User">
insert into `user`(
id, user_name, user_tel
) values (
#{id}, #{userName}, #{userTel}
)
</insert>
parameterMap.getType().getName(); 再通过parameterMap 获取到这个类型的全类名,在这个例子中,就是 User
- Class<?> parameterClass = Class.forName(parameterClassName); 通过反射,获取到这个类的Class对象
- Field[] declaredFields = parameterClass.getDeclaredFields(); 获取到这个类的所有字段,在后续调用该类内部的encrypt()方法,进行字段的加密判断。(为什么要调用实体类内部的,我想估计就是高内聚,低耦合吧)
- Method encrypt = parameterClass.getMethod(“encrypt”); // 通过反射,获取到这个类的 encrypt()方法,再通过invoke方法,调用这个方法,再内部进行字段的加密逻辑判断,因为是invoke(invocation.getArgs()[1])将传入mapper的对象传入到encrypt方法中进行加密,所以,方法执行完成之后,这个对象的属性也就变了,所以,后续直接 invocation.proceed(),执行mapper的逻辑即可
此时,利用 mybatis plugin 进行统一的拦截,进行 插入/更新的统一加密就已经完成了,再看看上述中调用的encrypt方法,加密的流程就大概告一段落@Override
public Object intercept(Invocation invocation) throws Throwable {
Object arg = invocation.getArgs()[0];
String className=arg.getClass().getName();
System.out.println(" 参数类型:"+className);
if(arg instanceof MappedStatement) {
//如果是第一个参数 MappedStatement
MappedStatement ms = (MappedStatement)arg;
SqlCommandType sqlCommandType = ms.getSqlCommandType();
System.out.println("操作类型:"+sqlCommandType);
if(sqlCommandType == SqlCommandType.INSERT || sqlCommandType==SqlCommandType.UPDATE) {
//如果是“增加”或“更新”操作,则继续进行默认操作信息赋值。否则,则退出
ParameterMap parameterMap = ms.getParameterMap();
String parameterClassName = parameterMap.getType().getName();
Class<?> parameterClass = Class.forName(parameterClassName);
Field[] declaredFields = parameterClass.getDeclaredFields();
Method encrypt = parameterClass.getMethod("encrypt");
System.out.println("before invocation.getArgs()[1]" + invocation.getArgs()[1]);
encrypt.invoke(invocation.getArgs()[1]);
System.out.println("after invocation.getArgs()[1]" + invocation.getArgs()[1]);
}
}
return invocation.proceed();
}
其实,encrypt方法也没几行代码,其实现逻辑如下,首先,自定义了一个 EncryptDecryptField,标注在方法上,表示这个字段是敏感信息, 当进行encrypt判断时,会通过反射,取出本类的所有field,循环判断是否标注了EncryptDecryptField 注解,如果标注了,即需要加密,这里,简单的使用UUID进行随机的表示,后续在此可以使用一些加密解密算法,来达到想要实现的效果。(附:这里再啰嗦解释一下对字段进行赋值(declaredField.set(this, UUID.randomUUID().toString().substring(0,6))) 这个代码,因为上述中是通过反射进行调用 invoke传入的就是mapper中的传参,所以,在此对this进行赋值,改的就是他,这个方法执行完毕,再调用 proceed执行mapper方法,此时的传参中的属性值已经进行变化,插入到数据库中的值也自然就是加密之后的值了)public void encrypt() throws IllegalAccessException {
Field[] declaredFields = this.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
// if is the Sensitive info field
if (declaredField.isAnnotationPresent(EncryptDecryptField.class)) {
declaredField.setAccessible(true);
declaredField.set(this, UUID.randomUUID().toString().substring(0,6));
}
}
}
到此,mybatis plugin 进行加密就已经完成,下面看看 查询时,通过 mybatis plugin进行解密
首先呢,还是新建一个Interceptor,拦截类型为 Executor, 拦截的方法是query
@Intercepts({
//执行参数接口,method为接口名称,args为参数对象(注意:不同版本个数不同,该版本:5.0.0)
@Signature(type= Executor.class, method="query", args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
public class QueryInterceptor implements Interceptor {
}
主要逻辑为,在查询结果之后,对结果集进行一些处理,例如下面,将查询的结果调用handleObject,进行处理,内部进行加密字段的判断,有,则调用解密函数,进行解析,否则,啥都不干
Object result = invocation.proceed();
// 查询的是一个列表
if (result instanceof List) {
List resultList = (List) result;
for (int i = 0; i < resultList.size(); i++) {
//依次获取其中的对象
Object object = resultList.get(i);
//利用反射对结果集中的每个对象进行处理,
handleObject(object, resultMapType);
resultList.set(i,object);
}
return resultList;
}
return result;