本身動手編寫一個Mybatis插件:Mybatis脫敏插件

1. 前言

在平常開發中,身份證號、手機號、卡號、客戶號等我的信息都須要進行數據脫敏。不然容易形成我的隱私泄露,客戶資料泄露,給不法分子可乘之機。可是數據脫敏不是把敏感信息隱藏起來,而是看起來像真的同樣,實際上不能是真的。我之前的公司就由於不重視脫敏,一名員工在離職的時候經過後臺的導出功能導出了核心的客戶資料賣給了競品,給公司形成了重大的損失。固然這裏有數據管理的緣由,可是脫敏仍舊是不可忽略的一環,脫敏能夠從必定程度上保證數據的合規使用。下面就是一份通過脫敏的數據:java

脫敏以後的數據

2. Mybatis 脫敏插件

最近在研究Mybatis的插件,因此考慮能不能在ORM中搞一搞脫敏,因此就嘗試了一下,這裏分享一下思路。藉此也分享一下Mybatis插件開發的思路。apache

2.1 Mybatis 插件接口

Mybatis中使用插件,須要實現接口org.apache.ibatis.plugin.Interceptor,以下所示:app

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)方法,這是咱們須要實現的方法。ide

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插件的粒度是精確到方法級別的,那麼疑問來了,插件如何知道輪到它工做了呢?this

因此Mybatis設計了簽名機制來解決這個問題,經過在插件接口上使用註解@Intercepts標註來解決這個問題。spa

@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 進行獲取。若是你以爲有用請無情的點贊。

關注公衆號:Felordcn 獲取更多資訊

我的博客:https://felord.cn

相關文章
相關標籤/搜索