Mybatis Interceptor 实现数据脱敏

拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑。 那么如果使用Mybatis对数据进行拦截,做一些满足自己需求的东西呢。今天我们就用这个实现一个数据的脱敏功能。

Interceptor接口

对于拦截器Mybatis为我们提供了一个Interceptor接口,通过实现该接口就可以定义我们自己的拦截器。首先我们先来看一下这个接口的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package org.apache.ibatis.plugin;

import java.util.Properties;

public interface Interceptor {

    Object intercept(Invocation invocation) throws Throwable;

    Object plugin(Object target); 

    void setProperties(Properties properties);

} 

可以看到,需要我们实现的方法有:

intercept拦截器的处理逻辑 plugin拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。 setProperties为插件设置properties属性

需求实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@Intercepts({
        @Signature(
                type = ResultSetHandler.class,
                method = "handleResultSets",
                args = {Statement.class}
        )
})
public class DataMaskingInterceptor implements Interceptor {  
   @Override
   public Object intercept(Invocation invocation) throws Throwable {
       // 获取到返回结果
       Object returnValue = invocation.proceed();
       if (returnValue != null) {
           // 对结果进行处理
           if (returnValue instanceof ArrayList<?>) {
               List<?> list = (ArrayList<?>) returnValue;
               for (int index = 0; index < list.size(); index++) {
                   Object returnItem = list.get(index);
                   if (returnItem != null) {
                       Class<?> clazz = returnItem.getClass();
                       Type superType = clazz.getGenericSuperclass();
                       if (superType.getClass().isInstance(Object.class)) {
                           List<Field> fieldList = new ArrayList<>();
                           // 利用反射获取所有字段
                           ReflectionUtils.doWithFields(clazz, fieldList::add);
                           for (Field field : fieldList) {
                                // 获取结果类上的注解
                               TableField annotation = field.getAnnotation(TableField.class);
                               if (annotation != null) {
                                    // 获取是否登记了脱敏 为什么要使用登记,而不使用注解呢?
                                   String dataMaskingFlag = DataShieldHelper.getDataMasking();
                                   if (!StringUtils.isEmpty(dataMaskingFlag)) {
                                       Class<? extends DataMasking> dataMasking = annotation.dataMasking();
                                       if (dataMasking != DataMasking.class) {
                                           DataMasking instance = getInstance(dataMasking);
                                           field.setAccessible(true);
                                           String value = (String) field.get(returnItem);
                                           value = instance.apply(value);
                                           ReflectionUtils.setField(field, returnItem, value);
                                       }
                                   }
                               }
                           }
                       }
                   }
               }
           }
       }
       return returnValue;
   }
   
    /**
    /* 固定写法 原理见 org.apache.ibatis.plugin.Plugin.wrap 方法
    **/
    @Override
    public Object plugin(Object target) {  
       return Plugin.wrap(target, this);  
    }

    @Override
    public void setProperties(Properties properties) {  
       
    }  

    private <T> T getInstance(Class<T> clazz) throws IllegalAccessException, InstantiationException {
        return clazz.newInstance();
    }

   
}

解释代码中为啥要使用DataShieldHelper来实现脱敏登记,我们在项目开发过程中,经常会用到Java对象复用的情况,那么如果在某个字段上加上脱敏注解,会出现实现问题,我们不能对拦截器去判断哪些查询需要脱敏哪些不需要,那么怎么才能实现需要时才对数据进行脱敏,某些情况不需要呢,此时ThreadLocal就能帮助到我们,ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
/* 线程工具类 主要用于登记脱敏操作
**/
public class DataShieldHelper {
    private static final ThreadLocal<String> LOCAL_DATA_MASKING = new ThreadLocal();

    public static void dataMasking(String symbol){
        if(symbol==null || symbol.isEmpty()){
            symbol = "*";
        }
        LOCAL_DATA_MASKING.set(symbol);
    }

    public static void dataMasking(){
        dataMasking("*");
    }

    public static String getDataMasking(){
        return LOCAL_DATA_MASKING.get();
    }

    public static void clearDataMasking(){
        LOCAL_DATA_MASKING.remove();
    }
}

使用@Intercepts 拦截ResultSetHandler接口中参数类型为StatementhandleResultSets方法 ResultSetHandler 主要负责将JDBC返回的ResultSet结果集对象转换成List类型的集合

拦截器的注册

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration  
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="config/jdbc.properties"></properties>
    <typeAliases>
        <package name="com.github.homeant.model" />
    </typeAliases>
    <plugins>
        <plugin interceptor="com.github.homeant.mybatis.interceptor.DataMaskingInterceptor">
            <!-- 属性 -->
            <property name="prop1" value="prop1" />
        </plugin>
    </plugins>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}" />
                <property name="url" value="${jdbc.url}" />
                <property name="username" value="${jdbc.username}" />
                <property name="password" value="${jdbc.password}" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/github/homeant/mapper/UserMapper.xml" />
    </mappers>
</configuration>

使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Test{
    @Test
    public void test(){
        // 脱敏登记
        DataShieldHelper.dataMasking();
        Optional<User> optional = userMapper.selectOn(user.getId());
        // 取消
        DataShieldHelper.clearDataMasking();
        optional.ifPresent(r->{
            log.debug("user:{}",r);
        });
    }
}

效果

可以看到,我们查询出来的数据已经脱敏了

1
2
3
4
2020-06-01 23:34:11.910 DEBUG 34392 --- [           main] c.g.h.d.s.mapper.UserMapper.selectOn     : ==>  Preparing: select id,username,password from t_user where id = ? 
2020-06-01 23:34:11.910 DEBUG 34392 --- [           main] c.g.h.d.s.mapper.UserMapper.selectOn     : ==> Parameters: 71(Integer)
2020-06-01 23:34:11.930 DEBUG 34392 --- [           main] c.g.h.d.s.mapper.UserMapper.selectOn     : <==      Total: 1
2020-06-01 23:34:11.940 DEBUG 34392 --- [           main] com.github.homeant.data.shield.DataTest  : user:User(id=71, username=tom, password=p@***********67)

上述为简易版代码,如果需要,请前往GitHub进行下载

代码直通车

data-shield