# 脱敏

返回:mybatis

脱敏

日常开发中,身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏

# Mybatis 插件接口

返回顶部

Mybatis中使用插件,需要实现接口org.apache.ibatis.plugin.Interceptor,如下所示:

package org.apache.ibatis.plugin;
import java.util.Properties;

/**
 * @author Clinton Begin
 */
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
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

最核心的是Object intercept(Invocation invocation)方法

# Invocation 对象

返回顶部

/**
 *    Copyright 2009-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author Clinton Begin
 */
public class Invocation {

  private final Object target;
  private final Method method;
  private final Object[] args;

  public Invocation(Object target, Method method, Object[] args) {
    this.target = target;
    this.method = method;
    this.args = args;
  }

  public Object getTarget() {
    return target;
  }

  public Method getMethod() {
    return method;
  }

  public Object[] getArgs() {
    return args;
  }

  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }
}
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

这个东西包含了四个概念

  • target 拦截的对象
  • method 拦截target中的具体方法,也就是说Mybatis插件的粒度是精确到方法级别的。
  • args 拦截到的参数。
  • proceed 执行被拦截到的方法,你可以在执行的前后做一些事情。

# 拦截签名

返回顶部

既然我们知道了Mybatis插件的粒度是精确到方法级别的,那么疑问来了,插件如何知道轮到它工作了呢?
所以Mybatis设计了签名机制来解决这个问题,通过在插件接口上使用注解@Intercepts标注来解决这个问题。

@Intercepts(@Signature(type = ResultSetHandler.class,
        method = "handleResultSets",
        args = {Statement.class}))
1
2
3

# 插件的作用域

返回顶部

那么问题又来了,Mybatis插件能拦截哪些对象,或者说插件能在哪个生命周期阶段起作用呢?

它可以拦截以下四大对象:

  • Executor 是SQL执行器,包含了组装参数,组装结果集到返回值以及执行SQL的过程,粒度比较粗。
  • StatementHandler 用来处理SQL的执行过程,我们可以在这里重写SQL非常常用。
  • ParameterHandler 用来处理传入SQL的参数,我们可以重写参数的处理规则。
  • ResultSetHandler 用于处理结果集,我们可以重写结果集的组装规则。

你需要做的就是明确的你的业务需要在上面四个对象的哪个处理阶段拦截处理即可。

# MetaObject

返回顶部

Mybatis提供了一个工具类org.apache.ibatis.reflection.MetaObject。它通过反射来读取和修改一些重要对象的属性。我们可以利用它来处理四大对象的一些属性,这是Mybatis插件开发的一个常用工具类。

常用方法

Object getValue(String name) 根据名称获取对象的属性值,支持OGNL表达式。 void setValue(String name, Object value) 设置某个属性的值。 Class<?> getSetterType(String name) 获取setter方法的入参类型。 Class<?> getGetterType(String name) 获取getter方法的返回值类型。

通常我们使用SystemMetaObject.forObject(Object object)来实例化MetaObject对象。

# Mybatis 脱敏插件实战

返回顶部

// 编写脱敏函数
public interface Desensitizer  extends Function<String,String>  {}
1
2
3
// 编写脱敏策略枚举
public enum SensitiveStrategy {
    /**
     * Username sensitive strategy.
     */
    USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
    /**
     * Id card sensitive type.
     */
    ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1****$2")),
    /**
     * Phone sensitive type.
     */
    PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),/**
     * Address sensitive type.
     */
    ADDRESS(s -> s.replaceAll("(\\S{8})\\S{4}(\\S*)\\S{4}", "$1****$2****"));
​​    private final Desensitizer desensitizer;SensitiveStrategy(Desensitizer desensitizer) {        this.desensitizer = desensitizer;
    }/**
     * Gets desensitizer.
     *
     * @return the desensitizer
     */
    public Desensitizer getDesensitizer() {
        return desensitizer;
    }}
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
// 编写脱敏字段的标记注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {
    SensitiveStrategy strategy();
}
1
2
3
4
5
6

我们的返回对象中如果某个字段需要脱敏,只需要通过标记就可以了

@Data
public class UserInfo {private static final long serialVersionUID = -8938650956516110149L;
    private Long userId;
    @Sensitive(strategy = SensitiveStrategy.USERNAME)
    private String name;
    private Integer age;
}
1
2
3
4
5
6
7
8

需要拦截的是ResultSetHandler对象的handleResultSets方法,我们只需要实现插件接口Interceptor并添加签名就可以了

@Slf4j
@Intercepts(@Signature(type = ResultSetHandler.class,
        method = "handleResultSets",
        args = {Statement.class}))
public class SensitivePlugin implements Interceptor {
    @SuppressWarnings("unchecked")
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        List<Object> records = (List<Object>) invocation.proceed();        // 对结果集脱敏
        records.forEach(this::sensitive);
        return records;
    }
​
​
    private void sensitive(Object source) {
        // 拿到返回值类型
        Class<?> sourceClass = source.getClass();
        // 初始化返回值类型的 MetaObject
        MetaObject metaObject = SystemMetaObject.forObject(source);
        // 捕捉到属性上的标记注解 @Sensitive 并进行对应的脱敏处理
        Stream.of(sourceClass.getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(Sensitive.class))
                .forEach(field -> doSensitive(metaObject, field));
    }
​
​
    private void doSensitive(MetaObject metaObject, Field field) {
        // 拿到属性名
        String name = field.getName();
        // 获取属性值
        Object value = metaObject.getValue(name);
        // 只有字符串类型才能脱敏  而且不能为null
        if (String.class == metaObject.getGetterType(name) && value != null) {
            Sensitive annotation = field.getAnnotation(Sensitive.class);
            // 获取对应的脱敏策略 并进行脱敏
            SensitiveStrategy type = annotation.strategy();
            Object o = type.getDesensitizer().apply((String) value);
            // 把脱敏后的值塞回去
            metaObject.setValue(name, o);
        }
    }
}
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

然后配置脱敏插件使之生效:

@Bean
public SensitivePlugin sensitivePlugin(){
    return new SensitivePlugin();
}
1
2
3
4

JSON序列化时

其实脱敏也可以在JSON序列化的时候进行。