1. 前言

在日常开发中,身份证号、手机号、卡号、客户号等个人信息都须要进行数据脱敏。否则容易造成个人隐私泄露,客户资料泄露,给不法分子可乘之机。然而数据脱敏不是把敏感信息暗藏起来,而是看起来像真的一样,实际上不能是真的。我以前的公司就因为不器重脱敏,一名员工在到职的时候通过后盾的导出性能导出了外围的客户资料卖给了竞品,给公司造成了重大的损失。当然这里有数据管理的起因,然而脱敏仍旧是不可疏忽的一环,脱敏能够从肯定水平上保证数据的合规应用。上面就是一份通过脱敏的数据:

2. Mybatis 脱敏插件

最近在钻研Mybatis的插件,所以思考能不能在ORM中搞一搞脱敏,所以就尝试了一下,这里分享一下思路。借此也分享一下Mybatis插件开发的思路。

2.1 Mybatis 插件接口

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

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  }}

这里其实最外围的是Object intercept(Invocation invocation)办法,这是咱们须要实现的办法。

2.2 Invocation 对象

那么外围办法中的Invocation 是个什么概念呢?

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);  }}

这个货色蕴含了四个概念:

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

2.3 拦挡签名

既然咱们晓得了Mybatis插件的粒度是准确到办法级别的,那么疑难来了,插件如何晓得轮到它工作了呢?

所以Mybatis设计了签名机制来解决这个问题,通过在插件接口上应用注解@Intercepts标注来解决这个问题。

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

就像下面一样,事实上就等于配置了一个Invocation

2.4 插件的作用域

那么问题又来了,Mybatis插件能拦挡哪些对象,或者说插件能在哪个生命周期阶段起作用呢?它能够拦挡以下四大对象:

  • ExecutorSQL执行器,蕴含了组装参数,组装后果集到返回值以及执行SQL的过程,粒度比拟粗。
  • StatementHandler 用来解决SQL的执行过程,咱们能够在这里重写SQL十分罕用。
  • ParameterHandler 用来解决传入SQL的参数,咱们能够重写参数的解决规定。
  • ResultSetHandler 用于处理结果集,咱们能够重写后果集的组装规定。

你须要做的就是明确的你的业务须要在下面四个对象的哪个解决阶段拦挡解决即可。

2.5 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对象。你会在接下来的实战DEMO中看到我应用它。

3. Mybatis 脱敏插件实战

接下来我就把结尾的脱敏需要实现一下。首先须要对脱敏字段进行标记并确定应用的脱敏策略。

编写脱敏函数:

/** * 具体策略的函数 * @author felord.cn * @since 11:24 **/public interface Desensitizer  extends Function<String,String>  {}

编写脱敏策略枚举:

/** * 脱敏策略. * * @author felord.cn * @since 11 :25 */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;    }}

编写脱敏字段的标记注解:

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public @interface Sensitive {    SensitiveStrategy strategy();}

咱们的返回对象中如果某个字段须要脱敏,只须要通过标记就能够了。例如上面这样:

@Datapublic class UserInfo {    private static final long serialVersionUID = -8938650956516110149L;    private Long userId;    @Sensitive(strategy = SensitiveStrategy.USERNAME)    private String name;    private Integer age;}

而后就是编写插件了,我能够确定的是须要拦挡的是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);        }    }}

而后配置脱敏插件使之失效:

@Beanpublic SensitivePlugin sensitivePlugin(){    return new SensitivePlugin();}

操作查问取得后果 UserInfo(userId=123123, name=李*龙, age=28) ,胜利将指定字段进行了脱敏。

补充一句,其实脱敏也能够在JSON序列化的时候进行。

4. 总结

明天对编写Mybatis插件的一些要点进行了阐明,同时依据阐明实现了一个脱敏插件。然而请留神肯定要相熟四大对象的生命周期,否则自写插件可能会造成意想不到的后果。插件能够关注:码农小胖哥 回复关键字 sensitive 进行获取。如果你感觉有用请有情的点赞。

关注公众号:Felordcn 获取更多资讯

集体博客:https://felord.cn