前言
在数字时代,数据安全问题备受关注。想象一下,你的应用程序可能在处理各种敏感信息,例如用户的身份证号码、银行卡号等。如果这些信息泄露,后果不堪设想!但别担心,今天我们将揭开 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表达式的创建。
- 避免不必要的对象创建:
- 如果传入的字符串不需要脱敏,或者已经是脱敏后的格式,可以直接返回原字符串,避免创建新的字符串对象。
效果图展示