需求:在实际项目开发中,有许多的敏感信息,例如用户的手机号,身份证等。我们不希望这些数据以明文的方式展示在数据库中,降低安全风险。所以,引申出了敏感信息的加密问题

那么,对于敏感信息的加密,普遍的做法为,在insert的时候,手动在代码中,对这些敏感数据进行加密,之后再执行insert操作,查询时,将这些信息进行解密,之后返回给前端,进行数据的展示。

但是这种做法有个非常不好的弊端,就是,但凡包含这些敏感信息的操作,都需要在代码中,手动添加这些加密/解密的算法,导致代码的膨胀。那么,有没有一种更好的方法,能够实现这些功能呢?

相信大家都会很自然的想到,aop,拦截器?没错,在mybatis中,利用plugin插件式开发,可以在mybatis执行时,先被我们自定义的interceptor进行拦截,执行完interceptor方法之后,再执行真正的数据库操作。那么,下面看看mybatis plugin 如何进行使用来完成此项需求

首先呢,要让需要集成mybatis中的Interceptor接口,其定义了拦截器的规范,其中的intercept(Invocation invocation) 方法中,就是自定义的拦截逻辑

  1. public interface Interceptor {
  2. Object intercept(Invocation invocation) throws Throwable;
  3. default Object plugin(Object target) {
  4. return Plugin.wrap(target, this);
  5. }
  6. default void setProperties(Properties properties) {
  7. // NOP
  8. }
  9. }

其次呢,需要在类上添加两个注解
其中的@Component 是 spring的注解,用于把这个类交给spring管理,这样mybatis 与spring整合的时候,就能用到这个组件。
然后是 @Intercepts() 注解,这个注解是 mybatis的,其中可以定义多个 @Signature(),用于拦截多种类型的sql语句
type为其拦截的类型,method 为其拦截的方法,args为这个拦截方法的传参类型。
其中type有四个类型可以传递

  • Executor, 执行器,其method可填方法如下

    1. int update(MappedStatement ms, Object parameter) throws SQLException;
    2. <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
    3. <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
    4. List<BatchResult> flushStatements() throws SQLException;
    5. void commit(boolean required) throws SQLException;
    6. void rollback(boolean required) throws SQLException;
    7. Transaction getTransaction();
    8. void close(boolean forceRollback);
    9. boolean isClosed();
  • ParameterHandler, 参数处理器,其method可填方法如下

    1. Object getParameterObject();
    2. void setParameters(PreparedStatement var1) throws SQLException;
  • ResultSetHandler, 返回结果集处理器,其method可填方法如下

    1. <E> List<E> handleResultSets(Statement stmt) throws SQLException;
    2. void handleOutputParameters(CallableStatement cs) throws SQLException;
  • StatementHandler,SQL语句处理器,其method可填方法如下

    1. Statement prepare(Connection var1, Integer var2) throws SQLException;
    2. void parameterize(Statement var1) throws SQLException;
    3. void batch(Statement var1) throws SQLException;
    4. int update(Statement var1) throws SQLException;
    5. <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;

    Intercept拦截器代码,其中有三个方法 intercept(),plugin(),setProperties()

    其中,intercept()是主要方法,其他先不看

    1. @Intercepts({
    2. //执行参数接口,method为接口名称,args为参数对象
    3. @Signature(type= Executor.class, method="update", args={MappedStatement.class, Object.class})
    4. })
    5. @Component
    6. public class InsertInterceptor implements Interceptor {
    7. @Override
    8. public Object intercept(Invocation invocation) throws Throwable {
    9. }
    10. @Override
    11. public Object plugin(Object target) {
    12. }
    13. @Override
    14. public void setProperties(Properties properties) {
    15. }
    16. }

    Intercept() 方法逻辑代码

  • 通过invocation.getArgs() 获取拦截器的参数

  • 其中[0] 第一个参数,根据它判断当前的操作类型。
  • 如果是insert or update,说明是新增操作,那么就需要对敏感信息进行一些的处理
  • 首先可以通过 ms.getParameterMap(); 获取到这个执行的传入参数,对应于 xml文件中的 parameterType

    1. <insert id="insertUser" parameterType="User">
    2. insert into `user`(
    3. id, user_name, user_tel
    4. ) values (
    5. #{id}, #{userName}, #{userTel}
    6. )
    7. </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的逻辑即可
    1. @Override
    2. public Object intercept(Invocation invocation) throws Throwable {
    3. Object arg = invocation.getArgs()[0];
    4. String className=arg.getClass().getName();
    5. System.out.println(" 参数类型:"+className);
    6. if(arg instanceof MappedStatement) {
    7. //如果是第一个参数 MappedStatement
    8. MappedStatement ms = (MappedStatement)arg;
    9. SqlCommandType sqlCommandType = ms.getSqlCommandType();
    10. System.out.println("操作类型:"+sqlCommandType);
    11. if(sqlCommandType == SqlCommandType.INSERT || sqlCommandType==SqlCommandType.UPDATE) {
    12. //如果是“增加”或“更新”操作,则继续进行默认操作信息赋值。否则,则退出
    13. ParameterMap parameterMap = ms.getParameterMap();
    14. String parameterClassName = parameterMap.getType().getName();
    15. Class<?> parameterClass = Class.forName(parameterClassName);
    16. Field[] declaredFields = parameterClass.getDeclaredFields();
    17. Method encrypt = parameterClass.getMethod("encrypt");
    18. System.out.println("before invocation.getArgs()[1]" + invocation.getArgs()[1]);
    19. encrypt.invoke(invocation.getArgs()[1]);
    20. System.out.println("after invocation.getArgs()[1]" + invocation.getArgs()[1]);
    21. }
    22. }
    23. return invocation.proceed();
    24. }
    此时,利用 mybatis plugin 进行统一的拦截,进行 插入/更新的统一加密就已经完成了,再看看上述中调用的encrypt方法,加密的流程就大概告一段落
    其实,encrypt方法也没几行代码,其实现逻辑如下,首先,自定义了一个 EncryptDecryptField,标注在方法上,表示这个字段是敏感信息, 当进行encrypt判断时,会通过反射,取出本类的所有field,循环判断是否标注了EncryptDecryptField 注解,如果标注了,即需要加密,这里,简单的使用UUID进行随机的表示,后续在此可以使用一些加密解密算法,来达到想要实现的效果。(附:这里再啰嗦解释一下对字段进行赋值(declaredField.set(this, UUID.randomUUID().toString().substring(0,6))) 这个代码,因为上述中是通过反射进行调用 invoke传入的就是mapper中的传参,所以,在此对this进行赋值,改的就是他,这个方法执行完毕,再调用 proceed执行mapper方法,此时的传参中的属性值已经进行变化,插入到数据库中的值也自然就是加密之后的值了)
    1. public void encrypt() throws IllegalAccessException {
    2. Field[] declaredFields = this.getClass().getDeclaredFields();
    3. for (Field declaredField : declaredFields) {
    4. // if is the Sensitive info field
    5. if (declaredField.isAnnotationPresent(EncryptDecryptField.class)) {
    6. declaredField.setAccessible(true);
    7. declaredField.set(this, UUID.randomUUID().toString().substring(0,6));
    8. }
    9. }
    10. }

到此,mybatis plugin 进行加密就已经完成,下面看看 查询时,通过 mybatis plugin进行解密

首先呢,还是新建一个Interceptor,拦截类型为 Executor, 拦截的方法是query

  1. @Intercepts({
  2. //执行参数接口,method为接口名称,args为参数对象(注意:不同版本个数不同,该版本:5.0.0)
  3. @Signature(type= Executor.class, method="query", args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
  4. })
  5. @Component
  6. public class QueryInterceptor implements Interceptor {
  7. }

主要逻辑为,在查询结果之后,对结果集进行一些处理,例如下面,将查询的结果调用handleObject,进行处理,内部进行加密字段的判断,有,则调用解密函数,进行解析,否则,啥都不干

  1. Object result = invocation.proceed();
  2. // 查询的是一个列表
  3. if (result instanceof List) {
  4. List resultList = (List) result;
  5. for (int i = 0; i < resultList.size(); i++) {
  6. //依次获取其中的对象
  7. Object object = resultList.get(i);
  8. //利用反射对结果集中的每个对象进行处理,
  9. handleObject(object, resultMapType);
  10. resultList.set(i,object);
  11. }
  12. return resultList;
  13. }
  14. return result;

参考
https://www.cnblogs.com/zhaodahai/p/6824252.html