注解实现数据脱敏操作
在项目中经常使用到数据脱敏,这里使用 fasterxml.jackson
来实现返回数据的脱敏操作
在这里借助 com.fasterxml.jackson.databind.JsonSerializer
接口来完成对数据序列化的时候的操作
*引入包文件*
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
一、创建策略
在序列化的时候需要指定序列化的规则,例如
字段长度 大于 10 前三后四明文字段长度 小于 4 无明文字段长度 大其他 后四明文
这里使用工具类来实现这个通用的操作
public class SensitiveUtils {
public static final String SYMBOL_STAR = "*";
/**
* 通用脱敏
* 字段长度 大于 10 前三后四明文
* 字段长度 小于 4 无明文
* 字段长度 大其他 后四明文
*
* @param sensitiveValue 敏感值
* @return 脱敏值
*/
public static String commonSensitive(String sensitiveValue) {
if (StringUtils.isBlank(sensitiveValue)) {
return sensitiveValue;
}
if (sensitiveValue.length() > 10) {
int s = sensitiveValue.length() - 7;
return sensitiveValue.replaceAll("(\\w{3})\\w{" + s + "}(\\w{4})", "$1" + getStars(s) + "$2");
} else if (sensitiveValue.length() < 4) {
return getStars(sensitiveValue.length());
} else {
int s = sensitiveValue.length() - 4;
return sensitiveValue.replaceAll("\\w{" + s + "}(\\w{4})", getStars(s) + "$1");
}
}
/**
* 判断是否是脱敏值
* 是脱敏值 返回 true
* 不是脱敏值 返回 false
*
* @param value 输入值
* @return 返回结果
*/
public static boolean simpleInvalidSensitive(String value) {
if (StringUtils.isBlank(value)) {
return false;
}
return value.contains(SYMBOL_STAR);
}
public static String getStars(int s) {
String stars = StringUtils.EMPTY;
for (int i = 0; i < s; i++) {
stars += SYMBOL_STAR;
}
return stars;
}
}
上面的是一个通用的工具类,下面,我们使用接口来实例化这个通用的策略:
首先是接口:
public interface MyStrategy {
String desensitizationByPattern(String source);
}
//然后构建一个默认的策略:
public class MyDefaultStrategy implements MyStrategy {
public String desensitizationByPattern(String source) {
return SensitiveUtils.commonSensitive(source);
}
}
这个操作就是传入一个字段返回脱敏后的字符串,到这里就完成了一个策略的制定
二、构建序列化注解
这个注解用来明确字段序列化时需要的序列化器和使用的策略,这里要用到注解 @JsonSerialize
,它的参数 using
用来明确使用的序列化器,这里使用自定义的序列化器,下一节介绍序列化器的创建。
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = MySensitiveInfoSerialize.class)
@Inherited
public @interface CommonSensitive {
/**
* 脱敏策略
*
* @return
*/
Class<? extends MyStrategy> strategy() default MyDefaultStrategy.class;
}
从上面可以看出来,这里的参数是一个接口性质的序列化策略,也就是说,后面是支持自定义的。
三、创建序列化器
序列化器要实现接口 com.fasterxml.jackson.databind.JsonSerializer
并实现方法
@Slf4j
@NoArgsConstructor
public class MySensitiveInfoSerialize extends JsonSerializer<String> implements
ContextualSerializer {
private MyStrategy myStrategy;
public MySensitiveInfoSerialize(MyStrategy myStrategy) {
this.myStrategy = myStrategy;
}
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(myStrategy.desensitizationByPattern(s));
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
// 非 String 类直接跳过
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
CommonSensitive sensitiveInfo = beanProperty.getAnnotation(CommonSensitive.class);
if (sensitiveInfo == null) {
sensitiveInfo = beanProperty.getContextAnnotation(CommonSensitive.class);
}
if (sensitiveInfo != null) {
Class<? extends MyStrategy> clazz = sensitiveInfo.strategy();
// 如果能得到注解,就将注解的 value 传入 SensitiveInfoSerialize
try {
return new MySensitiveInfoSerialize(clazz.getDeclaredConstructor().newInstance());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
}
return serializerProvider.findNullValueSerializer(null);
}
}
serialize
使用序列化器序列化字段
createContextual
创建序列化上下文,主要是明确使用的序列化策略或者其他方式
到这里基本就可以使用了
四、自定义序列化策略
从上面我们的策略是用接口的方式使用的,这里要实现这个接口
public class MyCustomizeNameStrategy implements MyStrategy {
@Override
public String desensitizationByPattern(String source) {
if (StringUtils.isNotBlank(source)) {
int s = source.length();
int ss = source.lastIndexOf(".");
if (ss == -1) {
ss = 1;
int sss = s - ss;
return source.replaceAll("(\\S{" + ss + "})\\S{" + sss + "}", "$1" + SensitiveUtils.getStars(sss));
} else {
int sss = s - ss;
return source.replaceAll("\\S{" + ss + "}(\\S{" + sss + "})", SensitiveUtils.getStars(ss) + "$1");
}
} else {
return source;
}
}
}
这个是用来序列化姓名的,保留姓,这样就完成了,在使用注解 @CommonSensitive
就可以使用了
五、规范化注解使用
在项目中经常要对不同性质的字段脱敏,例如邮箱,电话,身份证号码等,这些策略都不同,我们可以使用不同的序列化策略,但是这样使用不好的地方是我们要记住使用策略,为了方便使用,我们可以通过注解直接表明不同的注解表示不同的脱敏方式,无参的方式使用更加方便,这里选择使用邮件脱敏来说明
首先自定义一个邮件脱敏的规则:
public class MyEmailStrategy implements MyStrategy {
@Override
public String desensitizationByPattern(String source) {
if (StringUtils.isNotBlank(source)) {
int s = source.length();
int ss = source.indexOf("@");
int sss = s - ss;
return source.replaceAll("\\S{" + ss + "}(\\S{" + sss + "})", SensitiveUtils.getStars(ss)+"$1");
} else {
return source;
}
}
}
然后通过使用我们定义的原始注解包装一下
@Documented@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@CommonSensitive(strategy = MyEmailStrategy.class)@JacksonAnnotationsInsidepublic @interface MyEmailSensitive {}
这样就完成了对字段属性的序列化注解,使用的策略正是上面我们定义的,这个注解还是无参的。
六、使用案例
先建立一个序列化对象
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@CommonSensitive(strategy = MyEmailStrategy.class)
@JacksonAnnotationsInside
public @interface MyEmailSensitive {
}
//上面我们使用不同的使用方式脱敏
//创建一个请求接口来实现脱敏操作
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
@PostMapping("/submit2")
public Employee submit2(@RequestBody Employee employee) {
log.info(JSON.toJSONString(employee));
return employee;
}
}
测试使用报文:
{
"name": "王斌强",
"phone": "18637986236",
"email": "y.leffh@qymwmaej.yu"
}
打印日志:
{"email":"y.leffh@qymwmaej.yu","name":"王斌强","phone":"18637986236"}
返回报文:
{
"name": "王*",
"phone": "186****6236",
"email": "*******@qymwmaej.yu"
}
至此,完成了使用注解的方式完成字段的脱敏操作。