自己动手编写一个Mybatis插件:Mybatis脱敏插件

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

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

@Data
public 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);
        }
    }
}

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

@Bean
public SensitivePlugin sensitivePlugin(){
    return new SensitivePlugin();
}

操作查询获得结果 UserInfo(userId=123123, name=李*龙, age=28) ,成功将指定字段进行了脱敏。

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

4. 总结

今天对编写Mybatis插件的一些要点进行了说明,同时根据说明实现了一个脱敏插件。但是请注意一定要熟悉四大对象的生命周期,否则自写插件可能会造成意想不到的结果。插件可以关注:码农小胖哥 回复关键字 sensitive 进行获取。如果你觉得有用请无情的点赞。

关注公众号:码农小胖哥,获取更多资讯

个人博客:https://felord.cn

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342