简析
数据库脱敏(Data Masking)是一种数据安全技术,旨在保护敏感数据免受未经授权的访问。脱敏通过修改或隐藏数据库中的隐私数据,以便在开发、测试和分析环境中使用,同时保留原始数据的结构和格式。这样可以在不暴露真实数据的情况下,确保非生产环境中使用的数据与生产环境中的数据基本相似,并且可以满足合规性要求。
常用的需求场景: 比如前端展示个人信息时,邮箱,手机号等部分字段设置为*号,或者在某些财务类软件里,特殊的数据,比如工资,利润,金额啥的需要设置为*号,可以不让普通等级员工看到这些敏感信息,如果对高层领导做展示的话,可以设置一些特殊的接口单独对这些信息做查询。
实施脱敏
既然是优雅的对数据库返回信息进行脱敏,这里我们采用注解的方式对字段做操作,只需要在字段上面加上对应的注解,前端在获取接口结果的时候,这个字段的数据就会被隐藏起来,废话不多说,直接上代码。
代码
新建以下几个类
package com.example.demo.config;
import org.springframework.util.StringUtils;
public enum DataDealingWay {
/**
* 脱敏转换器
*/
NO_MASK((str, maskChar) -> {
return str;
}),
ALL_Data_MASK((str, maskChar) -> {
if (StringUtils.hasLength(str)) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
sb.append(StringUtils.hasLength(maskChar) ? maskChar : DataMaskingOperation.MASK_CHAR);
}
return sb.toString();
} else {
return str;
}
});
private final DataMaskingOperation operation;
private DataDealingWay(DataMaskingOperation operation) {
this.operation = operation;
}
public DataMaskingOperation operation() {
return this.operation;
}
}
package com.example.demo.config;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration(
proxyBeanMethods = false
)
public class DataMaskConfiguration {
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Jackson2ObjectMapperBuilder.class})
static class JacksonObjectMapperConfiguration {
JacksonObjectMapperConfiguration() {
}
@Bean
@Primary
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
AnnotationIntrospector ai = objectMapper.getSerializationConfig().getAnnotationIntrospector();
AnnotationIntrospector newAi = AnnotationIntrospectorPair.pair(ai, new DataMaskingAnnotationIntrospector());
objectMapper.setAnnotationIntrospector(newAi);
return objectMapper;
}
}
}
package com.example.demo.config;
public interface DataMaskingOperation {
String MASK_CHAR = "*";
String mask(String content, String maskChar);
}
package com.example.demo.config;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Objects;
public final class DataMaskingSerializer extends StdScalarSerializer<Object> {
private final DataMaskingOperation operation;
public DataMaskingSerializer() {
super(String.class, false);
this.operation = null;
}
public DataMaskingSerializer(DataMaskingOperation operation) {
super(String.class, false);
this.operation = operation;
}
public boolean isEmpty(SerializerProvider prov, Object value) {
String str = (String)value;
return str.isEmpty();
}
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (Objects.isNull(operation)) {
String content = DataDealingWay.ALL_Data_MASK.operation().mask((String) value, null);
gen.writeString(content);
} else {
String content = operation.mask((String) value, null);
gen.writeString(content);
}
}
public final void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSer) throws IOException {
this.serialize(value, gen, provider);
}
public JsonNode getSchema(SerializerProvider provider, Type typeHint) {
return this.createSchemaNode("string", true);
}
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
this.visitStringFormat(visitor, typeHint);
}
}
package com.example.demo.config;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
public class DataMaskingAnnotationIntrospector extends NopAnnotationIntrospector {
@Override
public Object findSerializer(Annotated am) {
DataDealing annotation = am.getAnnotation(DataDealing.class);
if (annotation != null) {
return new DataMaskingSerializer(annotation.dealDataWay().operation());
}
return null;
}
}
package com.example.demo.config;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataDealing {
DataDealingWay dealDataWay() default DataDealingWay.NO_MASK;
}
测试
我们新建一个实体类,在这个实体类的其实一个字段上面加上这个注解
package com.example.demo.entity;
import com.example.demo.config.DataDealing;
import com.example.demo.config.DataDealingWay;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class TestBean {
private String id;
private String name;
@DataDealing(dealDataWay = DataDealingWay.ALL_Data_MASK)
private String price;
}
新建一个controller类,使用这个字段做返回数据
package com.example.demo.controller;
import com.example.demo.entity.TestBean;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/test")
public class TestBeanContrller {
@PostMapping("/list")
public Map<String,Object> list(){
Map<String,Object> map =new HashMap<String, Object>();
TestBean bean = new TestBean();
bean.setId("1").setName("马保国").setPrice("5.0");
map.put("data", bean);
return map;
}
}
效果展示
我们可以看到price字段的价格已经被隐藏了
以上结束,另外代码还可以进行修改,比如可以限制脱敏的长度,可以指定循环次数达到长度固定的效果,目前是数据字段有多长,星号就会有多长,在实际应用中可能需要固定一下长度,这样数据展示比较美观。