• 【SpringBoot】几种常见的数据脱敏方案


    需求场景:

    对于某些接口返回的信息,涉及到敏感数据的必须进行脱敏操作

    如:
    在这里插入图片描述

    用户的手机号不能直接显示,需要脱敏。

    方案一、SQL 数据脱敏实现

    -- CONCAT()LEFT()RIGHT()字符串函数组合使用,请看下面具体实现
     
    -- CONCAT(str1,str2,):返回结果为连接参数产生的字符串
    -- LEFT(str,len):返回从字符串str 开始的len 最左字符
    -- RIGHT(str,len):从字符串str 开始,返回最右len 字符
     
    -- 电话号码脱敏sql:
     
    SELECT mobilePhone AS oldPhone, CONCAT(LEFT(mobilePhone,3), '********' ) AS newPhone FROM t_s_user
     
    -- 身份证号码脱敏sql:
     
    SELECT idcard AS oldIdCard, CONCAT(LEFT(idcard,3), '****' ,RIGHT(idcard,4)) AS newIdCard FROM t_s_user
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    方案二、JAVA数据脱敏实现

    查看 github:

    https://gitee.com/strong_sea/sensitive-plus

    方案三、自定义注解实现

    思路:要做成可配置多策略的脱敏操作,要不然一个个接口进行脱敏操作,重复的工作量太多。定义数据脱敏注解和数据脱敏逻辑的接口, 在返回类上,对需要进行脱敏的属性加上,并指定对应的脱敏策略操作。

    自定义注解类似于 @JsonFormat

    代码:

    1、接口规范:

    public interface DataMaskOperation {
    
        // 脱敏方法
        String mask(String str, String maskChar);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2、枚举类:

    public enum DataMaskEnum {
    
        // 不脱敏
        NO_MASK((str, maskChar) -> str)
        ,
        // 全脱敏
        ALL_MASK((str, maskChar) ->{
            if (StringUtils.hasLength(str)) {
                StringBuilder builder = new StringBuilder();
                for (int i = 0; i < str.length(); i++) {
                    builder.append(StringUtils.hasLength(maskChar) ? maskChar : "*");
                }
                return builder.toString();
            }
            return str;
        })
        ;
    
        // 成员变量  是一个接口类型
        private DataMaskOperation operation;
    
        DataMaskEnum(DataMaskOperation operation) {
            this.operation = operation;
        }
    
        public DataMaskOperation operation() {
            return this.operation;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    每一个枚举实例都重写了上述的 mask() 方法:表示每一个实例代表着不同的脱敏规则。这里有两个:不脱敏、全脱敏。大家可以根据自己的想法添加其它的脱敏规则(我只想脱敏一部分数据,并指明从哪开始,从哪结束)。当然,也可以按照自己的想法修改 mask() 方法,它只是一个规范(可以给此方法添加一个参数,或者删除一个参数~~~)

    3、自定义 Serializer

    参考 jackson 的 StringSerializer,下面的示例只针对 String 类型进行脱敏

    public final class DataMaskingSerializer extends StdScalarSerializer<Object> {
    
        private final DataMaskOperation operation;
    
        public DataMaskingSerializer() {
            super(String.class, false);
            this.operation = null;
        }
    
        public DataMaskingSerializer(DataMaskOperation operation) {
            super(String.class, false);
            this.operation = operation;
        }
    
        @Override
        public boolean isEmpty(SerializerProvider prov, Object value) {
            String str = (String)value;
            return str.isEmpty();
        }
    
        @Override
        public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            if (Objects.isNull(operation)) {
                String content = DataMaskEnum.ALL_MASK.operation().mask((String) value, null);
                gen.writeString(content);
            } else {
                String content = operation.mask((String) value, null);
                gen.writeString(content);
            }
        }
    
        @Override
        public final void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSer) throws IOException {
            this.serialize(value, gen, provider);
        }
    
        @Override
        public JsonNode getSchema(SerializerProvider provider, Type typeHint) {
            return this.createSchemaNode("string", true);
        }
    
        @Override
        public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
            this.visitStringFormat(visitor, typeHint);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    4、自定义 AnnotationIntrospector,适配我们自定义注解返回相应的Serializer

    public class DataMaskAnnotationIntrospector extends NopAnnotationIntrospector{
        @Override
        public Object findSerializer(Annotated am) {
            DataMask annotation = am.getAnnotation(DataMask.class);
            if (annotation != null) {
                return new DataMaskingSerializer(annotation.maskFunc().operation());
            }
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    5、覆盖ObjectMapper

    @Configuration
    public class DataMaskConfiguration {
    
        @Configuration
        @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 DataMaskAnnotationIntrospector());
                objectMapper.setAnnotationIntrospector(newAi);
                return objectMapper;
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    6、使用注解

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class MyDataMaskVo {
    
        private Integer id;
    
        @DataMask(maskFunc = DataMaskEnum.NO_MASK)
        private String name;
        @DataMask(maskFunc = DataMaskEnum.ALL_MASK)
        private String number;
    
        private String address;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    @RestController
    @RequestMapping("/data/mask")
    public class DataMaskController {
    
        @GetMapping("/list")
        public ResultVo<MyDataMaskVo> list() {
            MyDataMaskVo v1 = new MyDataMaskVo(1, "zzc", "13217251369", "河南省信阳市");
            MyDataMaskVo v2 =new MyDataMaskVo(2, "wzc", "13217251369", "北京市朝阳区");
            MyDataMaskVo v3 =new MyDataMaskVo(3, "wxc", "13217251369", "浙江省杭州市");
            return ResultVoUtil.success(Arrays.asList(v1, v2, v3));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    优化:

    由于 Java 8 中新增了许多函数式接口,所以,这里就不需要我们自定义接口了,可以直接使用函数式接口。

    @Target({ElementType.FIELD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @JacksonAnnotationsInside
    @JsonSerialize(using = DataMaskingSerializer2.class)
    public @interface DataMask2 {
    
        DataMaskEnum2 function();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    public enum DataMaskEnum2 {
    
        /**
         * 名称脱敏
         */
        USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2"))
        ,
        /**
         * Phone sensitive type.
         */
        PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"))
        ,
        /**
         * Address sensitive type.
         */
        ADDRESS(s -> s.replaceAll("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****"))
        ;
    
        /**
         * 成员变量  是一个接口类型
         */
        private Function<String, String> function;
    
        DataMaskEnum2(Function<String, String> function) {
            this.function = function;
        }
    
        public Function<String, String> function() {
            return this.function;
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    借助Jackson类和接口实现序列化才脱敏:

    public final class DataMaskingSerializer2 extends JsonSerializer<String> implements ContextualSerializer {
    
        private DataMaskEnum2 dataMaskEnum2;
    
        @Override
        public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            gen.writeString(dataMaskEnum2.function().apply(value));
        }
    
        @Override
        public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
            DataMask2 annotation = property.getAnnotation(DataMask2.class);
            if (Objects.nonNull(annotation)&&Objects.equals(String.class, property.getType().getRawClass())) {
                this.dataMaskEnum2 = annotation.function();
                return this;
            }
            return prov.findValueSerializer(property.getType(), property);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    public class MyDataMaskVo2 {
    
        private Integer id;
    
        @DataMask2(function = DataMaskEnum2.USERNAME)
        private String name;
        @DataMask2(function = DataMaskEnum2.PHONE)
        private String number;
        @DataMask2(function = DataMaskEnum2.ADDRESS)
        private String address;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    接口测试:

    @RestController
    @RequestMapping("/data/mask")
    public class DataMaskController {
    
        @GetMapping("/list2")
        public ResultVo<MyDataMaskVo> list2() {
            MyDataMaskVo2 v1 = new MyDataMaskVo2(1, "zzc", "13217251369", "河南省信阳市");
            MyDataMaskVo2 v2 =new MyDataMaskVo2(2, "wzc", "13217251369", "北京市朝阳区");
            MyDataMaskVo2 v3 =new MyDataMaskVo2(3, "wxc", "13217251369", "浙江省杭州市");
            return ResultVoUtil.success(Arrays.asList(v1, v2, v3));
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
  • 相关阅读:
    springboot+影院售票小程序 毕业设计-附源码111154
    企业电子杂志如何制作与分享
    cpp文件操作
    html picture元素
    电脑为什么会卡、CPU和内存的重要性、选哪个好呢?
    app商品详情原数据 API ——淘宝/天猫
    让我们重新认识一下docker 的全局观
    WeetCode2滑动窗口系列
    获取 Linux Standard Base (LSB) 版本信息
    物联网AI MicroPython传感器学习 之 TEA5767 FM收音机模块
  • 原文地址:https://blog.csdn.net/sco5282/article/details/126642636