mybatis自制插件+注解实现数据脱敏
- 前言
- 数据脱敏的实现方式
- 构思
- 从哪个地方进行脱敏?
- 它怎么知道我什么数据需要脱敏
- 项目实现
- 拦截器实现
- 注解实现
- 枚举实现
- 效果图展示
前言
在数字时代,数据安全问题备受关注。想象一下,你的应用程序可能在处理各种敏感信息,例如用户的身份证号码、银行卡号等。如果这些信息泄露,后果不堪设想!但别担心,今天我们将揭开 MyBatis 数据脱敏的神秘面纱,让你的数据像戴着隐形护甲一样安全。
数据脱敏的实现方式
我认为数据脱敏主要可分为两种情况。首先,是在数据入库时进行脱敏处理,这意味着在存储之前就对敏感数据进行加密,比如可以使用类似于密码盐加密的方式进行加密。第二种情况是在从数据库查询出数据后进行脱敏处理。在这种情况下,脱敏的位置可以灵活选择,可以在查询结果立即脱敏,也可以在控制器层面进行脱敏处理。举例来说,可以利用AOP在方法执行后执行脱敏逻辑。这里我主要讲的是第二种中的查询结果立即脱敏。
构思
从哪个地方进行脱敏?
首先对于mybatis来说,它其实也是遵守像传统的JDBC操作的,只不过它在这其中点了几朵花。具体来说也就是下面的几步:
- Class.forName注册驱动
- 获取一个Connection对象
- 创建一个Statement对象
- execute()方法执行SQL语句,获取ResultSet结果集
- 通过ResultSet结果集给POJO的属性赋值
- 最后关闭相关的资源
通过上面的,我们就能知道我们需要拦截的位置了,也就是在ResultSet结果集那里,在mybatis中也就是
org.apache.ibatis.executor.resultset.ResultSetHandler#handleResultSets
方法
具体来说,ResultSetHandler
是 MyBatis 中的一个接口,它定义了数据库查询结果集处理的方法。其中,handleResultSets
方法用于处理从数据库返回的结果集。在 MyBatis 中,查询结果可以是单个对象、对象列表或映射,而 handleResultSets
方法则负责将这些查询结果转换为 Java 对象或集合。
它怎么知道我什么数据需要脱敏
在1的基础上我们需要明白,如何找到你标记为脱敏的数据,以及你如何标记脱敏。多想一步的话,我们就应该知道,我们脱敏的可能目前仅仅有手机号,身份证号,但是保不准以后就会有别的了,而且单纯的在拦截器中根据字段名称编码也不现实,于是就有了注解,比如对于user
表中的phone
我们需要脱敏,那么只需要在实体类的这个字段下加个注解即可
项目实现
这里我就不再过多的赘述了,直接贴代码
拦截器实现
package com.todoitbo.baseSpringbootDasmart.interceptor;
import com.todoitbo.baseSpringbootDasmart.annotation.Desensitize;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;
import java.util.Properties;
/**
* @author xiaobo
*/
@Intercepts({@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
)})
@Component
public class DesensitizeInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 执行结果处理前的逻辑
Object result = invocation.proceed();
// 对结果进行脱敏处理
if (result instanceof List) {
List<?> list = (List<?>) result;
for (Object obj : list) {
desensitize(obj);
}
} else {
desensitize(result);
}
return result;
}
private void desensitize(Object obj) {
if (obj == null) {
return;
}
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
// 检查字段上是否存在Desensitize注解
if (field.isAnnotationPresent(Desensitize.class)) {
Desensitize desensitize = field.getAnnotation(Desensitize.class);
try {
// 私有字段可以访问
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String) {
// 字段脱敏
String desensitizedValue = desensitize.type().desensitize((String) value);
// 设置脱敏后的值
field.set(obj, desensitizedValue);
}
} catch (IllegalAccessException e) {
// 处理异常
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以通过配置文件传入参数
}
}
注解实现
package com.todoitbo.baseSpringbootDasmart.annotation;
import com.todoitbo.baseSpringbootDasmart.Enum.DesensitizeType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author xiaobo
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Desensitize {
// 定义脱敏策略,可以扩展更多类型
DesensitizeType type() default DesensitizeType.PHONE;
}
枚举实现
package com.todoitbo.baseSpringbootDasmart.Enum;
import java.util.function.Function;
/**
* @author todoitbo
* @date 2024/4/12
*/
// 脱敏策略枚举
public enum DesensitizeType {
PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
EMAIL(s -> s.replaceAll("(\\w+)\\w{3}@(\\w+)", "$1***@$2"));
// ...其他脱敏类型
private final Function<String, String> desensitizer;
DesensitizeType(Function<String, String> desensitizer) {
this.desensitizer = desensitizer;
}
public String desensitize(String value) {
return desensitizer.apply(value);
}
private String desensitizeValue(String value, DesensitizeType type) {
return type.desensitize(value);
}
// 可以添加更多的脱敏类型
}
如果你追求特别完美,或者极致,你也可以优化上面的代码,具体从以下几点优化:
- 预编译正则表达式:
- 每次调用
desensitize
方法时,都会创建一个新的正则表达式模式。 - 预编译正则表达式,并将它们作为
Pattern
对象存储,可以减少正则表达式编译的开销。
- 每次调用
- 减少lambda表达式创建的开销:
- 每个枚举实例都会创建一个lambda表达式。
- 可以考虑将脱敏逻辑移到一个静态方法中,并在枚举构造器里引用这个方法,减少lambda表达式的创建。
- 避免不必要的对象创建:
- 如果传入的字符串不需要脱敏,或者已经是脱敏后的格式,可以直接返回原字符串,避免创建新的字符串对象。