美文网首页
聊聊如何自定义数据脱敏

聊聊如何自定义数据脱敏

作者: linyb极客之路 | 来源:发表于2021-12-28 10:02 被阅读0次

    前言

    什么是数据脱敏

    数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护

    常用脱敏规则

    替换、重排、加密、截断、掩码

    良好的数据脱敏实施

    1、尽可能地为脱敏后的应用,保留脱敏前的有意义信息
    2、最大程度地防止黑客进行破解

    今天我们聊聊如何自定义数据脱敏

    整体思路

    本示例通过替换的手段实现脱敏,然后配合常用的框架特性,比如mybatis的拦截器机制或者json的序列化来快速实现脱敏

    具体落地

    1、定义一个脱敏工具类

    可以直接引用hutool工具包,不过它在5.6+版本以上才提供了这个工具
    https://www.hutool.cn/docs/#/core/工具类/信息脱敏工具-DesensitizedUtil

    不然就自己实现一个,形如下

    public class DesensitizedUtils {
    
    
    
        /**
         * 脱敏,使用默认的脱敏策略
         * <pre>
         * DesensitizedUtil.desensitized("100", DesensitizedUtils.DesensitizedType.USER_ID)) =  "0"
         * DesensitizedUtil.desensitized("段正淳", DesensitizedUtils.DesensitizedType.CHINESE_NAME)) = "段**"
         * DesensitizedUtil.desensitized("51343620000320711X", DesensitizedUtils.DesensitizedType.ID_CARD)) = "5***************1X"
         * DesensitizedUtil.desensitized("09157518479", DesensitizedUtils.DesensitizedType.FIXED_PHONE)) = "0915*****79"
         * DesensitizedUtil.desensitized("18049531999", DesensitizedUtils.DesensitizedType.MOBILE_PHONE)) = "180****1999"
         * DesensitizedUtil.desensitized("北京市海淀区马连洼街道289号", DesensitizedUtils.DesensitizedType.ADDRESS)) = "北京市海淀区马********"
         * DesensitizedUtil.desensitized("duandazhi-jack@gmail.com.cn", DesensitizedUtils.DesensitizedType.EMAIL)) = "d*************@gmail.com.cn"
         * DesensitizedUtil.desensitized("1234567890", DesensitizedUtils.DesensitizedType.PASSWORD)) = "**********"
         * DesensitizedUtil.desensitized("苏D40000", DesensitizedUtils.DesensitizedType.CAR_LICENSE)) = "苏D4***0"
         * DesensitizedUtil.desensitized("11011111222233333256", DesensitizedUtils.DesensitizedType.BANK_CARD)) = "1101 **** **** **** 3256"
         * </pre>
         *
         * @param str              字符串
         * @param desensitizedType 脱敏类型;可以脱敏:用户id、中文名、身份证号、座机号、手机号、地址、电子邮件、密码
         * @return 脱敏之后的字符串
         * @author dazer and neusoft and qiaomu
         * @since 5.6.2
         */
        public static String desensitized(CharSequence str, DesensitizedType desensitizedType) {
            if (StrUtil.isBlank(str)) {
                return StrUtil.EMPTY;
            }
            String newStr = String.valueOf(str);
            switch (desensitizedType) {
                case USER_ID:
                    newStr = String.valueOf(DesensitizedUtils.userId());
                    break;
                case CHINESE_NAME:
                    newStr = DesensitizedUtils.chineseName(String.valueOf(str));
                    break;
                case ID_CARD:
                    newStr = DesensitizedUtils.idCardNum(String.valueOf(str), 1, 2);
                    break;
                case FIXED_PHONE:
                    newStr = DesensitizedUtils.fixedPhone(String.valueOf(str));
                    break;
                case MOBILE_PHONE:
                    newStr = DesensitizedUtils.mobilePhone(String.valueOf(str));
                    break;
                case ADDRESS:
                    newStr = DesensitizedUtils.address(String.valueOf(str), 8);
                    break;
                case EMAIL:
                    newStr = DesensitizedUtils.email(String.valueOf(str));
                    break;
                case PASSWORD:
                    newStr = DesensitizedUtils.password(String.valueOf(str));
                    break;
                case CAR_LICENSE:
                    newStr = DesensitizedUtils.carLicense(String.valueOf(str));
                    break;
                case BANK_CARD:
                    newStr = DesensitizedUtils.bankCard(String.valueOf(str));
                    break;
                default:
            }
            return newStr;
        }
    
        /**
         * 【用户id】不对外提供userId
         *
         * @return 脱敏后的主键
         */
        public static Long userId() {
            return 0L;
        }
    
        /**
         * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**
         *
         * @param fullName 姓名
         * @return 脱敏后的姓名
         */
        public static String chineseName(String fullName) {
            if (StrUtil.isBlank(fullName)) {
                return StrUtil.EMPTY;
            }
            return StrUtil.hide(fullName, 1, fullName.length());
        }
    
        /**
         * 【身份证号】前1位 和后2位
         *
         * @param idCardNum 身份证
         * @param front     保留:前面的front位数;从1开始
         * @param end       保留:后面的end位数;从1开始
         * @return 脱敏后的身份证
         */
        public static String idCardNum(String idCardNum, int front, int end) {
            //身份证不能为空
            if (StrUtil.isBlank(idCardNum)) {
                return StrUtil.EMPTY;
            }
            //需要截取的长度不能大于身份证号长度
            if ((front + end) > idCardNum.length()) {
                return StrUtil.EMPTY;
            }
            //需要截取的不能小于0
            if (front < 0 || end < 0) {
                return StrUtil.EMPTY;
            }
            return StrUtil.hide(idCardNum, front, idCardNum.length() - end);
        }
    
        /**
         * 【固定电话 前四位,后两位
         *
         * @param num 固定电话
         * @return 脱敏后的固定电话;
         */
        public static String fixedPhone(String num) {
            if (StrUtil.isBlank(num)) {
                return StrUtil.EMPTY;
            }
            return StrUtil.hide(num, 4, num.length() - 2);
        }
    
        /**
         * 【手机号码】前三位,后4位,其他隐藏,比如135****2210
         *
         * @param num 移动电话;
         * @return 脱敏后的移动电话;
         */
        public static String mobilePhone(String num) {
            if (StrUtil.isBlank(num)) {
                return StrUtil.EMPTY;
            }
            return StrUtil.hide(num, 3, num.length() - 4);
        }
    
        /**
         * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****
         *
         * @param address       家庭住址
         * @param sensitiveSize 敏感信息长度
         * @return 脱敏后的家庭地址
         */
        public static String address(String address, int sensitiveSize) {
            if (StrUtil.isBlank(address)) {
                return StrUtil.EMPTY;
            }
            int length = address.length();
            return StrUtil.hide(address, length - sensitiveSize, length);
        }
    
        /**
         * 【电子邮箱】邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com
         *
         * @param email 邮箱
         * @return 脱敏后的邮箱
         */
        public static String email(String email) {
            if (StrUtil.isBlank(email)) {
                return StrUtil.EMPTY;
            }
            int index = StrUtil.indexOf(email, '@');
            if (index <= 1) {
                return email;
            }
            return StrUtil.hide(email, 1, index);
        }
    
        /**
         * 【密码】密码的全部字符都用*代替,比如:******
         *
         * @param password 密码
         * @return 脱敏后的密码
         */
        public static String password(String password) {
            if (StrUtil.isBlank(password)) {
                return StrUtil.EMPTY;
            }
            return StrUtil.repeat('*', password.length());
        }
    
        /**
         * 【中国车牌】车牌中间用*代替
         * eg1:null       -》 ""
         * eg1:""         -》 ""
         * eg3:苏D40000   -》 苏D4***0
         * eg4:陕A12345D  -》 陕A1****D
         * eg5:京A123     -》 京A123     如果是错误的车牌,不处理
         *
         * @param carLicense 完整的车牌号
         * @return 脱敏后的车牌
         */
        public static String carLicense(String carLicense) {
            if (StrUtil.isBlank(carLicense)) {
                return StrUtil.EMPTY;
            }
            // 普通车牌
            if (carLicense.length() == 7) {
                carLicense = StrUtil.hide(carLicense, 3, 6);
            } else if (carLicense.length() == 8) {
                // 新能源车牌
                carLicense = StrUtil.hide(carLicense, 3, 7);
            }
            return carLicense;
        }
    
        /**
         * 银行卡号脱敏
         * eg: 1101 **** **** **** 3256
         *
         * @param bankCardNo 银行卡号
         * @return 脱敏之后的银行卡号
         * @since 5.6.3
         */
        public static String bankCard(String bankCardNo) {
            if (StrUtil.isBlank(bankCardNo)) {
                return bankCardNo;
            }
            bankCardNo = StrUtil.trim(bankCardNo);
            if (bankCardNo.length() < 9) {
                return bankCardNo;
            }
    
            final int length = bankCardNo.length();
            final int midLength = length - 8;
            final StringBuilder buf = new StringBuilder();
    
            buf.append(bankCardNo, 0, 4);
            for (int i = 0; i < midLength; ++i) {
                if (i % 4 == 0) {
                    buf.append(CharUtil.SPACE);
                }
                buf.append('*');
            }
            buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length);
            return buf.toString();
        }
    }
    

    其实正常到这个步骤,通过替换实现脱敏就可以完成,可以直接在程序中,直接调用这个工具就行。但是作为一个懂得偷懒的程序员,肯定不满足这样。于是我们会进一步封装

    2、自定义脱敏注解

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Sensitive {
    
        DesensitizedType strategy() default DesensitizedType.NONE;
    
        /**
         * 是否使用dfa算法
         * @return
         */
        boolean useDFA() default false;
    
        /**
         * dfa敏感字符替换,默认替换成 "*"
         * @return
         */
        String dfaReplaceChar() default "*";
    
    
        /**
         * dfa敏感字符替换次数
         * @return
         */
        int dfaReplaceCharRepeatCount() default 1;
    
    }
    
    

    3、利用一些框架特性提升效率

    a、如果项目已经有用mybatis,则可以利用mybatis拦截器特性。实现原理就是拦截响应回来的结果,然后对结果进行脱敏处理

    @Intercepts(@Signature(type = ResultSetHandler.class,method = "handleResultSets",args = Statement.class))
    public class DesensitizedInterceptor implements Interceptor {
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            List<Object> list = (List<Object>) invocation.proceed();
            list.forEach(EntityUtils::desensitized);
    
            return list;
        }
    
    }
    

    b、 如果项目是基于springboot的web项目,则可以利用springboot自带的jackson自定义序列化实现。它的实现原来其实就是在json进行序列化渲染给前端时,进行脱敏。

    如果是这种方案,则需对自定义注解进行改造一下,加上

    @JacksonAnnotationsInside
    @JsonSerialize(using = DesensitizedJsonSerializer.class)
    

    注解。形如下

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @JacksonAnnotationsInside
    @JsonSerialize(using = DesensitizedJsonSerializer.class)
    public @interface Sensitive {
    
        DesensitizedType strategy() default DesensitizedType.NONE;
    
        /**
         * 是否使用dfa算法
         * @return
         */
        boolean useDFA() default false;
    
        /**
         * dfa敏感字符替换,默认替换成 "*"
         * @return
         */
        String dfaReplaceChar() default "*";
    
    
        /**
         * dfa敏感字符替换次数
         * @return
         */
        int dfaReplaceCharRepeatCount() default 1;
    
    }
    
    

    序列化脱敏逻辑核心代码如下

    public class DesensitizedJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
    
        private Sensitive sensitive;
        @Override
        public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeString(EntityUtils.getDesensitizedValue(sensitive,s));
    
        }
    
        @Override
        public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
    
            sensitive = beanProperty.getAnnotation(Sensitive.class);
    
            if(!ObjectUtils.isEmpty(sensitive) && String.class.isAssignableFrom(beanProperty.getType().getRawClass())){
                return this;
            }
            return serializerProvider.findValueSerializer(beanProperty.getType(),beanProperty);
        }
    }
    
    

    示例

    以json那种方式为例

    1、定义实体对象,需要进行脱敏的属性上加上脱敏注解

    @Data
    @EqualsAndHashCode(callSuper = false)
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public class UserDTO {
    
        private Integer id;
    
        private String username;
    
        @Sensitive(strategy = DesensitizedType.PASSWORD)
        private String password;
    
        @Sensitive(strategy = DesensitizedType.CHINESE_NAME)
        private String fullname;
    
        @Sensitive(strategy = DesensitizedType.MOBILE_PHONE)
        private String mobile;
    
        @Sensitive(strategy = DesensitizedType.EMAIL)
        private String email;
    
        @Sensitive(useDFA = true,dfaReplaceChar = "#",dfaReplaceCharRepeatCount = 3)
        private String remark;
    }
    
    

    2、编写一个测试controller

    @RestController
    @RequestMapping("/user")
    public class UserController {
    
        @Autowired
        private UserService userService;
    
    
        @GetMapping(value="/list")
        public AjaxResult listUsers(){
           return AjaxResult.success(userService.listUserDTO());
        }
    
    }
    
    

    测试结果

    image

    如图所示已经进行脱敏

    其他方案

    1、基于Sharding Sphere实现数据脱敏

    具体实现可以参考如下文章

    https://jaskey.github.io/blog/2020/03/18/sharding-sphere-data-desensitization/

    2、自定义注解格式化

    主要实现步骤如下

    • 1、实现AnnotationFormatterFactory接口

    • 2、创建脱敏格式化类实现Formatter

    • 3、将AnnotationFormatterFactory实现的接口注册到FormatterRegistry

    具体实现可以参考如下文章

    https://blog.csdn.net/qq_27081015/article/details/103295983

    4、利用fastjson进行脱敏

    主要实现步骤如下

    • 1、实现ValueFilter接口,在process进行脱敏

    • 2、配置fastjson为默认JSON转换

    /**
         * 配置fastjson为默认JSON转换
         *
         * @return
         */
        @Bean
        public HttpMessageConverters fastJsonHttpMessageConverters() {
            // 1.定义一个converters转换消息的对象
            FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
            // 2.添加fastjson的配置信息,比如: 是否需要格式化返回的json数据
            FastJsonConfig fastJsonConfig = new FastJsonConfig();
            fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
            fastJsonConfig.setSerializeFilters(new ValueDesensitizeFilter());//添加自己写的拦截器
            // 3.在converter中添加配置信息
            fastConverter.setFastJsonConfig(fastJsonConfig);
            // 4.将converter赋值给HttpMessageConverter
            HttpMessageConverter<?> converter = fastConverter;
            // 5.返回HttpMessageConverters对象
            return new HttpMessageConverters(converter);
        }
    
    

    具体实现可以参考如下文章

    https://blog.csdn.net/qq_27081015/article/details/103297316

    5、利用mybatis-mate

    mybatis-plus 企业(数据优雅处理)模块,使用时要配置一下授权码。如下

    mybatis-mate:
      cert:
        grant: jinTianYiXueKe
        license: GKXP9r4MCJhGID/DTGigcBcLmZjb1YZGjE4GXaAoxbtGsPC20sxpEtiUr2F7Nb1ANTUekvF6Syo6DzraA4M4oacwoLVTglzfvaEyUogW8L7mydqlsZ4+hlm20kK85eLJK1QsskrSJmreMnEaNh9lsV7Lpbxy9JeGCeM0HPEbRvq8Y+8dUt5bQYLklsa3ZIBexir+4XykZY15uqn1pYIp4pEK0+aINTa57xjJNoWuBIqm7BdFIb4l1TAcPYMTsMXhF5hfMmKD2h391HxWTshJ6jbt4YqdKD167AgeoM+B+DE1jxlLjcpskY+kFs9piOS7RCcmKBBUOgX2BD/JxhR2gQ==
    

    他的实现机理就是利用json序列化那种,如果感兴趣可以参考如下链接

    https://gitee.com/baomidou/mybatis-mate-examples

    本文的demo也有基于mybatis-mate实现脱敏,链接如下
    https://github.com/lyb-geek/springboot-learning/tree/master/springboot-desensitization/springboot-desensitzation-mybatis-mate

    总结

    有时候业务场景的实现方式有多种多样,大家要懂得取舍判断,比如上面的方案如果你的项目本来就没用mybatis,但为了脱敏又引入mybatis,这种方案就额外有加入了复杂度,后面维护估计就有得折腾了

    demo链接

    https://github.com/lyb-geek/springboot-learning/tree/master/springboot-desensitization

    相关文章

      网友评论

          本文标题:聊聊如何自定义数据脱敏

          本文链接:https://www.haomeiwen.com/subject/mgfpqrtx.html