
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插件能攔截哪些物件,或者說插件能在哪個生命周期階段起作用呢?它可以攔截以下四大物件:
- Executor 是SQL執行器,包含了組裝引數,組裝結果集到回傳值以及執行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 = https://www.cnblogs.com/felordcn/p/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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/86691.html
標籤:Java
