如何优雅的对数据库返回数据进行脱敏

简析

  数据库脱敏(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字段的价格已经被隐藏了

以上结束,另外代码还可以进行修改,比如可以限制脱敏的长度,可以指定循环次数达到长度固定的效果,目前是数据字段有多长,星号就会有多长,在实际应用中可能需要固定一下长度,这样数据展示比较美观。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值