@Auto-Annotation自定义注解——数据脱敏篇
自定义通用注解连更系列—连载中…
首页介绍:点这里
前言
在日常业务开发中,我们会接触到各种各样的数据,这些数据可能包含用户敏感信息。如果这些敏感信息在传输和存储过程中被泄露,将会给用户带来不必要的麻烦和安全隐患。因此,数据脱敏技术的应用变得越来越重要。
数据脱敏技术
数据脱敏又称数据去隐私化或数据变形,是在给定的规则、策略下对敏感数据进行变换、修改的技术机制,能够在很大程度上解决敏感数据在非可信环境中使用的问题。根据数据保护规范和脱敏策略.对业务数据中的敏感信息实施自动变形.实现对敏感信息的隐藏。
具体需求为:手机号码隐藏 156****2635、姓名脱敏:张** 等等
本文将介绍一种基于自定义注解的数据脱敏技术——使用@SensitiveFiled注解实现。
实现思路:
-
在日常开发中我们经常用到对响应数据进行序列化的操作。
比如:由于前端没有Long类型,Long类型返回给前端会造成精度丢失问题,我们就需要用到这个注解将其转为字符串
@JsonSerialize(using = ToStringSerializer.class)
-
随后我就想能不能参考他的实现,来实现对字符串进行一些特殊处理,进行脱敏返回呢。
-
翻看源码得之,原来是重写JsonSerializer.serialize()便能对数据进行序列化操作,说干就干。
实现目标:
通过自定义注解,标记属性,注明使用的脱敏对象即可实现对该属性进行数据脱敏。
@SensitiveFiled(using = NameSensitiveAction.class)
private String name;
所需依赖
spring-boot-starter-web自带该依赖,没有则导入以下依赖:
<!--jackson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>
数据脱敏注解@SensitiveFiled
在实现数据脱敏技术之前,我们需要先定义一个注解@SensitiveFiled用于标记需要进行脱敏的字段。这个注解的定义如下:
@Retention(RetentionPolicy.RUNTIME) 表示该注解在运行时生效
@Target(ElementType.FIELD) 表示注解的作用目标 ElementType.FIELD表示注解作用于字段上
@JacksonAnnotationsInside 这个注解用来标记Jackson复合注解,当你使用多个Jackson注解组合成一个自定义注解时会用到它
@JsonSerialize(using = SensitiveJsonSerializer.class) 指定使用自定义的序列化器
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveFiledSerialize.class)
public @interface SensitiveFiled {
/**
* 脱敏实现类
*/
Class<? extends ISensitiveTypeStrategy> using();
/**
* 脱敏前缀位数(自定义脱敏使用)
*/
int prefixHideLen() default 0;
/**
* 脱敏后缀位数(自定义脱敏使用)
*/
int suffixHideLen() default 0;
/**
* 脱敏符号(自定义脱敏使用)
*/
String symbol() default "*";
}
继承序列化器
实现ContextualSerializer接口重写createContextual(),扫描脱敏注解创建序列化对象并注入属性中
继承JsonSerializer类重写serialize()方法,我看网上很多文章都是采用根据不同的枚举类型实现对应的数据脱敏方法,这样新加一个枚举就得修改一次代码。显得极不优雅。这里我才用工厂模式+策略模式实现高扩展性。
public class SensitiveFiledSerialize extends JsonSerializer<String> implements ContextualSerializer {
/**
* 脱敏前缀位数
*/
private int prefixHideLen;
/**
* 脱敏后缀位数
*/
private int suffixHideLen;
/**
* 脱敏符号
*/
private String symbol;
/**
* 脱敏策略实现对象
*/
private ISensitiveTypeStrategy strategist;
public SensitiveFiledSerialize() {
}
public SensitiveFiledSerialize(int prefixHideLen, int suffixHideLen, String symbol, ISensitiveTypeStrategy strategist) {
this.prefixHideLen = prefixHideLen;
this.suffixHideLen = suffixHideLen;
this.symbol = symbol;
this.strategist = strategist;
}
/**
* 1、重写createContextual()
* 扫描脱敏注解创建序列化对象并注入属性
*
* @param serializerProvider 序列化对象
* @param beanProperty bean属性对象
* @return json序列化对象
* @throws JsonMappingException 异常
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
SensitiveFiled annotation = beanProperty.getAnnotation(SensitiveFiled.class);
if (annotation == null) {
annotation = beanProperty.getContextAnnotation(SensitiveFiled.class);
}
if (annotation != null) {
Class<? extends ISensitiveTypeStrategy> aClass = annotation.using();
ISensitiveTypeStrategy strategist = DesensitizationFactory.getDesensitization(aClass);
return new SensitiveFiledSerialize(
annotation.prefixHideLen(),
annotation.suffixHideLen(),
annotation.symbol(),
strategist
);
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(null);
}
/**
* 2、重写serialize()
* 根据不同的枚举类型实现对应的数据脱敏方法
*
* @param str 元数据
* @param jsonGenerator json生成器
* @param serializerProvider 序列化对象
* @throws IOException 异常
*/
@Override
public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
assert strategist != null;
String sensitiveData;
if (strategist instanceof CustomerSensitiveAction){
sensitiveData = strategist.sensitiveData(str, prefixHideLen, suffixHideLen, symbol);
}else {
sensitiveData = strategist.sensitiveData(str);
}
jsonGenerator.writeString(sensitiveData);
}
}
脱敏策略接口
通过策略模式实现对个性化字段的脱敏,可实现该接口自定义脱敏策略,提高扩展性。
/**
* 脱敏策略模型
*
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
public interface ISensitiveTypeStrategy {
/**
* 数据脱敏方法
*
* @param origin 原始数据
* @return 脱敏数据
*/
String sensitiveData(String origin);
/**
* 数据脱敏方法
*
* @param origin 原始数据
* @param prefixHideLen 前缀
* @param suffixHideLen 后缀
* @param symbol 符号
* @return 脱敏数据
*/
default String sensitiveData(String origin, Integer prefixHideLen, Integer suffixHideLen, String symbol) {
if (origin == null) {
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0, n = origin.length(); i < n; i++) {
if (i < prefixHideLen) {
sb.append(origin.charAt(i));
continue;
}
if (i > (n - suffixHideLen - 1)) {
sb.append(origin.charAt(i));
continue;
}
sb.append(symbol);
}
return sb.toString();
}
}
策略接口实现类
/** 名称脱敏
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
public class NameSensitiveAction implements ISensitiveTypeStrategy {
@Override
public String sensitiveData(String origin) {
return this.sensitiveData(origin,1,0,"*");
}
}
/** 电话号码脱敏
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
public class PhoneSensitiveAction implements ISensitiveTypeStrategy {
@Override
public String sensitiveData(String origin) {
return this.sensitiveData(origin, 3, 4, "*");
}
}
/** 身份证脱敏
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
public class IdCardSensitiveAction implements ISensitiveTypeStrategy {
@Override
public String sensitiveData(String origin) {
return origin.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1*****$2");
}
}
/** 自定义脱敏
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
public class CustomerSensitiveAction implements ISensitiveTypeStrategy {
@Override
public String sensitiveData(String origin) {
return origin;
}
}
策略工厂类
通过测试发现依赖注入的方式无法将策略者对象注入进序列化器中,通过工厂模式实现对不同策略对象动态实例化,避免将策略者写死在代码中的情况。
/**
*策略类的工厂
*/
public class DesensitizationFactory {
private DesensitizationFactory() {
}
/**
* 这里采用一个 Map 集合 对指定的策略类进行缓存 避免对象的重复创建
*/
private static final Map<Class<?>, ISensitiveTypeStrategy> MAP = new HashMap<>();
@SuppressWarnings("all")
public static ISensitiveTypeStrategy 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 (ISensitiveTypeStrategy) clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new UnsupportedOperationException(e.getMessage(), e);
}
});
}
}
标记脱敏参数
@Data
public class User {
@SensitiveFiled(using = NameSensitiveAction.class)
private String name;
@SensitiveFiled(using = CustomerSensitiveAction.class,prefixHideLen = 1,suffixHideLen = 4,symbol = "#")
private String age;
}
到此,自定义注解实现数据脱敏已全部完成。
这时有人问到,脱敏前缀位数、脱敏符号不是给自定义脱敏使用吗,那为什么自定义脱敏类CustomerSensitiveAction
怎么什么都没做呢?
其实该类只是一个标识类,程序运行时并不会走进该类的方法中,注意我在SensitiveFiledSerialize
重写serialize()
时,有个细节,判断了是否为自定义注解实现类,是则走接口中的默认方法。
if (strategist instanceof CustomerSensitiveAction){
sensitiveData = strategist.sensitiveData(str, prefixHideLen, suffixHideLen, symbol);
}
总结:
- 在进行序列化的时候,框架先扫描到了实体类的该注解 @SensitiveFiled(using = NameSensitiveAction.class)
- 然后根据该注解里面的 @JsonSerialize(using = SensitiveFiledSerialize.class) 使用了我们自定义的序列化器
- 先执行了createContextual方法,来获取上下文(获取注解里面的参数 NameSensitiveAction.class)
- 接着通过策略工厂实例化出该对象,并对属性进行赋值
- 然后执行序列化方法serialize(),该方法会获取前面的createContextual方法返回的参数,以及策略者对象。
- 根据不同策略者对象执行不同的脱敏方法,对数据进行脱敏操作
- 最后通过jsonGenerator.writeString(sensitiveData)方法然后把返回值设置给序列化的对象
- 重而达到对数据修改后响应给客户端的操作