• (二十六)admin-boot项目之基于注解的数据字段脱敏


    项目地址:https://gitee.com/springzb/admin-boot
    如果觉得不错,给个 star

    简介:
    这是一个基础的企业级基础后端脚手架项目,主要由springboot为基础搭建,后期整合一些基础插件例如:redis、xxl-job、flowable、minioio、easyexcel、skyWalking、rabbitmq

    (二十六)基于注解的数据字段脱敏

    基础项目地址:

    https://gitee.com/springzb/admin-boot

    一、整体思路

    基于字段注解的方式,确定字段是否需要脱敏以及字段数据脱敏类型,最终通过AOP的方式也就是返回数据之前序列化的时候进行数据脱敏

    二、具体实现

    PrivacyTypeEnum 定义脱敏数据类型

    package cn.mesmile.admin.common.desensitization;
    
    /**
     * @author zb
     * @Description 脱敏数据字段类型
     */
    public enum  PrivacyTypeEnum {
    
        /** 自定义(此项需设置脱敏的范围)*/
        CUSTOMER,
    
        /** 姓名 */
        NAME,
    
        /** 身份证号 */
        ID_CARD,
    
        /** 手机号 */
        PHONE,
    
        /** 邮箱 */
        EMAIL
    
    }
    
    
    • 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

    PrivacyEncrypt 定义数据脱敏注解

    package cn.mesmile.admin.common.desensitization;
    
    import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
    import com.fasterxml.jackson.databind.annotation.JsonSerialize;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author zb
     * @Description 数据脱敏注解
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    // 表示自定义自己的注解PrivacyEncrypt
    @JacksonAnnotationsInside
    // 该注解使用序列化的方式
    @JsonSerialize(using = PrivacySerializer.class)
    public @interface PrivacyEncrypt {
    
        /**
         * 脱敏数据类型(没给默认值,所以使用时必须指定type)
         */
        PrivacyTypeEnum type();
    
        /**
         * 前置不需要打码的长度
         */
        int prefixNoMaskLen() default 1;
    
        /**
         * 后置不需要打码的长度
         */
        int suffixNoMaskLen() default 1;
    
        /**
         * 用什么打码
         */
        String symbol() default "*";
    }
    
    
    • 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

    PrivacyUtil 数据脱敏工具类

    package cn.mesmile.admin.common.desensitization;
    
    /**
     * @author zb
     * @Description 脱敏工具类
     */
    public class PrivacyUtil {
    
        /**
         * 隐藏手机号中间四位
         */
        public static String hidePhone(String phone) {
            return phone.replaceAll("(\\\\d{3})\\\\d{4}(\\\\d{4})", "$1****$2");
        }
    
        /**
         * 隐藏邮箱
         */
        public static String hideEmail(String email) {
            return email.replaceAll("(\\\\w?)(\\\\w+)(\\\\w)(@\\\\w+\\\\.[a-z]+(\\\\.[a-z]+)?)", "$1****$3$4");
        }
    
        /**
         * 隐藏身份证
         */
        public static String hideIDCard(String idCard) {
            return idCard.replaceAll("(\\\\d{4})\\\\d{10}(\\\\w{2})", "$1*****$2");
        }
    
        /**
         * 【中文姓名】只显示第一个汉字,其他隐藏为星号,比如:任**
         */
        public static String hideChineseName(String chineseName) {
            if (chineseName == null) {
                return null;
            }
            return desValue(chineseName, 1, 0, "*");
        }
    
    
        /**
         * 对字符串进行脱敏操作
         * @param origin          原始字符串
         * @param prefixNoMaskLen 左侧需要保留几位明文字段
         * @param suffixNoMaskLen 右侧需要保留几位明文字段
         * @param maskStr         用于遮罩的字符串, 如'*'
         * @return 脱敏后结果
         */
        public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {
            if (origin == null) {
                return null;
            }
            StringBuilder sb = new StringBuilder();
            for (int i = 0, n = origin.length(); i < n; i++) {
                if (i < prefixNoMaskLen) {
                    sb.append(origin.charAt(i));
                    continue;
                }
                if (i > (n - suffixNoMaskLen - 1)) {
                    sb.append(origin.charAt(i));
                    continue;
                }
                sb.append(maskStr);
            }
            return sb.toString();
        }
    
    }
    
    
    • 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
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    PrivacySerializer 具体脱敏序列化实现(AOP)

    package cn.mesmile.admin.common.desensitization;
    
    import cn.mesmile.admin.common.exceptions.ServiceException;
    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.databind.BeanProperty;
    import com.fasterxml.jackson.databind.JsonMappingException;
    import com.fasterxml.jackson.databind.JsonSerializer;
    import com.fasterxml.jackson.databind.SerializerProvider;
    import com.fasterxml.jackson.databind.ser.ContextualSerializer;
    import lombok.AllArgsConstructor;
    import lombok.NoArgsConstructor;
    import org.apache.commons.lang3.StringUtils;
    
    import java.io.IOException;
    import java.util.Objects;
    
    /**
     * @author zb
     * @Description aop的方式序列化的时候进行脱敏
     *      如果在保存数据的时候使用此注解的类会把处理好的数据保存到数据库从而导致数据不准确
     *      有时候查询的数据需要部署脱敏的数据而是原数据 解决:可以多声明一个类的字段,
     *      如idCardNumber2代表脱敏数据,而idCardNumber代表原数据即可
     */
    @NoArgsConstructor
    @AllArgsConstructor
    public class PrivacySerializer extends JsonSerializer<String> implements ContextualSerializer {
    
        /** 脱敏类型 */
        private PrivacyTypeEnum privacyTypeEnum;
        /** 前几位不脱敏 */
        private Integer prefixNoMaskLen;
        /** 最后几位不脱敏 */
        private Integer suffixNoMaskLen;
        /** 用什么打码 */
        private String symbol;
    
        @Override
        public void serialize(final String origin, final JsonGenerator jsonGenerator,
                              final SerializerProvider serializerProvider) throws IOException {
            if (StringUtils.isNotBlank(origin) && null != privacyTypeEnum) {
                switch (privacyTypeEnum) {
                    case CUSTOMER:
                        jsonGenerator.writeString(PrivacyUtil.desValue(origin, prefixNoMaskLen, suffixNoMaskLen, symbol));
                        break;
                    case NAME:
                        jsonGenerator.writeString(PrivacyUtil.hideChineseName(origin));
                        break;
                    case ID_CARD:
                        jsonGenerator.writeString(PrivacyUtil.hideIDCard(origin));
                        break;
                    case PHONE:
                        jsonGenerator.writeString(PrivacyUtil.hidePhone(origin));
                        break;
                    case EMAIL:
                        jsonGenerator.writeString(PrivacyUtil.hideEmail(origin));
                        break;
                    default:
                        throw new ServiceException("unknown privacy type enum " + privacyTypeEnum);
                }
            }
        }
    
        @Override
        public JsonSerializer<?> createContextual(final SerializerProvider serializerProvider,
                                                  final BeanProperty beanProperty) throws JsonMappingException {
            if (beanProperty != null) {
                if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
                    PrivacyEncrypt privacyEncrypt = beanProperty.getAnnotation(PrivacyEncrypt.class);
                    if (privacyEncrypt == null) {
                        privacyEncrypt = beanProperty.getContextAnnotation(PrivacyEncrypt.class);
                    }
                    if (privacyEncrypt != null) {
                        return new PrivacySerializer(privacyEncrypt.type(), privacyEncrypt.prefixNoMaskLen(),
                                privacyEncrypt.suffixNoMaskLen(), privacyEncrypt.symbol());
                    }
                }
                return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
            }
            return serializerProvider.findNullValueSerializer(null);
        }
    }
    
    
    • 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
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    注意事项

    • 如果在保存数据的时候使用此注解的类会把处理好的数据保存到数据库从而导致数据不准确

    • 有时候查询的数据需要部署脱敏的数据而是原数据 解决:可以多声明一个类的字段, 如idCardNumber2代表脱敏数据,而idCardNumber代表原数据即可

    三、测试

    public class Student implements Serializable {
    
        private String id;
    
        /***
         *  测试数据脱敏
         */
        @PrivacyEncrypt(type = PrivacyTypeEnum.NAME)
        private String name;
    
        private Integer age;
    
        private Double score;
    
        private LocalDate birthday;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    @GetMapping("/get")
    public R get(){
        List<Student> test6 = new ArrayList<>();
        Student student = new Student();
        student.setAge(23);
        student.setBirthday(LocalDate.now());
        student.setId("123");
        student.setName("张三");
        test6.add(student);
        return R.data(test6);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    脱敏数据成功:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7vtJ3HLh-1662456148736)(image/image_4lCnYCtL4D.png)]

  • 相关阅读:
    openstack——5、对搭建的虚拟机拓展(使用私有网络、自主创建镜像)
    人工衍射透镜的设计与分析
    【英语:基础高阶_经典外刊阅读】L5.解构阅读中段落—匹配类题目详解
    sc60 硬件设计
    3D视觉识别案例:3D无序棒料抓取,阀体圆环上下料,电机定子上料
    阿里面试面试题
    【html5期末大作业】基于HTML+CSS+JavaScript管理系统页面模板
    【DataRoom】- 基于VUE的开源的大屏可视化设计器
    20、Python -- 变量作用域、局部函数
    mvcc机制中的快照读和当前读
  • 原文地址:https://blog.csdn.net/suprezheng/article/details/126729503