Jackson序列化方式实现数据脱敏
方案选择
- 基于Mybatis 的拦截器:对select 语句进行拦截数据脱敏,但是存在问题 在某些业务中对数据脱敏字段是需要进行逻辑业务处理的
- 基于Jaskson 序列化:针对需要展示在前端的数据 通常我们都是对应一个VO对象,这里可以在将VO对象返回给前端序列化时进行数据的脱敏
实现方案
1.自定义序列化器 覆盖原来的序列化方式
需要引入jackson依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.0</version>
</dependency>
/**
* 脱敏序列化器
*/
public class ObjectDesensitizeSerializer extends StdSerializer<Object> implements ContextualSerializer {
protected ObjectDesensitizeSerializer() {
super(Object.class);
}
@Override
public JsonSerializer<Object> createContextual(SerializerProvider prov, BeanProperty property) {
// 这里创建序列化上下文环境 以选择是否需要指定返回不同的序列化器
return serializer;
}
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// 这里编写实际序列化的字段的处理
}
}
2.定义策略类(处理实际脱敏逻辑)
然后针对不同的字段会有不同的脱敏方案 比如 身份证号 银行卡号 电话号码等
这里采用 策略模式 + 工厂模式 进行解耦,符合开闭原则
定义一个顶级接口 然后多个实现
/**
* 定义一个 顶级的脱敏器
*/
public interface Desensitization<T> {
/**
* 脱敏实现
*
* @param target 脱敏对象
* @return 脱敏返回结果
*/
T desensitize(T target);
}
// 多接口实现
/**
* 字符串脱敏器
*/
public interface StringDesensitization extends Desensitization<String> {
}
/**
* 身份证(18位和15位) 脱敏器
*/
public class IDCardDesensitization implements StringDesensitization {
}
/**
* 邮箱脱敏器 默认只保留域名
*/
public class EmailDesensitization implements StringDesensitization {
}
这里只展示具体的一个脱敏实现 其他都是类似
/**
* 手机号脱敏器 默认只保留前3位和后4位
*/
public class PhoneDesensitization implements StringDesensitization {
/**
* 手机号正则
*/
private static final Pattern DEFAULT_PATTERN = Pattern.compile("(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\d{8}");
/**
* 手机号脱敏 只保留前3位和后4位
*/
@Override
public String desensitize(String target) {
// 匹配判断是否符合正则
Matcher matcher = DEFAULT_PATTERN.matcher(target);
while (matcher.find()) {
String group = matcher.group();
// 调用方法进行脱敏
target = target.replace(group, group.substring(0, 3) + Symbol.getSymbol(4, Symbol.STAR) + group.substring(7, 11));
}
return target;
}
}
public class Symbol {
/**
* '*'脱敏符
*/
public static final String STAR = "*";
private Symbol() {
}
/**
* 获取符号
*
* @param number 符号个数
* @param symbol 符号
*/
public static String getSymbol(int number, String symbol) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < number; i++) {
sb.append(symbol);
}
return sb.toString();
}
}
3.定义工厂类 获取实际策略 并缓存策略
上述是 策略模式的体现 ,策略模式 通常结合工厂模式 屏蔽创建对象细节 直接通过工厂创建指定的策略类
/**
*策略类的工厂
*/
public class DesensitizationFactory {
private DesensitizationFactory() {
}
// 这里采用一个 Map 集合 对指定的策略类进行缓存 避免对象的重复创建
private static final Map<Class<?>, Desensitization<?>> map = new HashMap<>();
@SuppressWarnings("all")
public static Desensitization<?> getDesensitization(Class<?> clazz) {
// 如果传递的只是接口 不是实现类 则抛出异常
if (clazz.isInterface()) {
throw new UnsupportedOperationException("desensitization is interface, what is expected is an implementation class !");
}
return map.computeIfAbsent(clazz, k -> {
try {
// 返回指定 Class 的策略类 同时缓存在 Map 中
return (Desensitization<?>) clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new UnsupportedOperationException(e.getMessage(), e);
}
});
}
}
4.定义注解 简化使用
然后就是注解的定义,利用注解 直接对 VO 对象的字段标记,无代码入侵
/**
* 对象脱敏 注解
*/
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) // 作用于注解类型上 供其他注解使用
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside // 组合注解 将多个注解拼在一起使用
@JsonSerialize(using = ObjectDesensitizeSerializer.class) // 标记序列化配置中 使用哪个类序列化 这里指定之前定义的ObjectDesensitizeSerializer
@Documented
public @interface Desensitize {
// 这里对应了工厂类中的Class类型 以及在 ObjectDesensitizeSerializer 中创建上下文环境时 可供获取的参数去选择实际的脱敏方式
/**
* 对象脱敏器实现
*/
@SuppressWarnings("all")
Class<? extends Desensitization<?>> desensitization();
}
然后就是对应的实际字段注解
/**
* 电话脱敏 注解
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@Desensitize(desensitization = PhoneDesensitization.class) // 这里指定执行脱敏逻辑的类
@Documented
public @interface PhoneDesensitize {
}
/**
* 中华人民共和国身份证 脱敏注解
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@Desensitize(desensitization = IDCardDesensitization.class) // 这里指定执行脱敏逻辑的类
@Documented
public @interface IDCardDesensitize {
}
5.完善自定义序列化器的逻辑
最后就是回到开头 编写实际的 自定义序列化器
**
* 脱敏序列化器
*/
public class ObjectDesensitizeSerializer extends StdSerializer<Object> implements ContextualSerializer {
// 面向接口 根据策略不同而实际 set 不同的 脱敏类
private transient Desensitization<Object> desensitization;
protected ObjectDesensitizeSerializer() {
super(Object.class);
}
public Desensitization<Object> getDesensitization() {
return desensitization;
}
public void setDesensitization(Desensitization<Object> desensitization) {
this.desensitization = desensitization;
}
// 创建上下文环境
@Override
public JsonSerializer<Object> createContextual(SerializerProvider prov, BeanProperty property) {
// 根据 BeanProperty 获取被标记 VO 字段的注解上的 实际策略脱敏类
Desensitize annotation = property.getAnnotation(Desensitize.class);
return createContextual(annotation.desensitization());
}
@SuppressWarnings("unchecked")
public JsonSerializer<Object> createContextual(Class<? extends Desensitization<?>> clazz) {
ObjectDesensitizeSerializer serializer = new ObjectDesensitizeSerializer();
// 判断是否属于 StringDesensitization 因为 StringDesensitization 属于全脱敏
if (clazz != StringDesensitization.class) {
// 不属于则 回到上面 通过工厂类去创建脱敏类
serializer.setDesensitization((Desensitization<Object>) DesensitizationFactory.getDesensitization(clazz));
}
return serializer;
}
// 创建完 上下文环境 返回 serializer 执行序列化 serialize
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
Desensitization<Object> objectDesensitization = getDesensitization();
// 获取 策略类(即 前面 set 的 desensitization)
if (objectDesensitization != null) {
try {
// 不为空调用处理脱敏逻辑
gen.writeObject(objectDesensitization.desensitize(value));
} catch (Exception e) {
gen.writeObject(value);
}
} else if (value instanceof String) {
// 为空 说明是 StringDesensitization 且字段是 String 类型 则 全脱敏
gen.writeString(Symbol.getSymbol(((String) value).length(), Symbol.STAR));
} else {
// 否则 正常序列化
gen.writeObject(value);
}
}
}
最终可达到 一个注解 即可脱敏