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 {@Overridepublic 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) {// 处理异常}}}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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表达式的创建。
- 避免不必要的对象创建:
- 如果传入的字符串不需要脱敏,或者已经是脱敏后的格式,可以直接返回原字符串,避免创建新的字符串对象。