关于java:聊聊如何自定义数据脱敏

53次阅读

共计 10351 个字符,预计需要花费 26 分钟才能阅读完成。

前言

什么是数据脱敏

数据脱敏是指对某些敏感信息通过脱敏规定进行数据的变形,实现敏感隐衷数据的牢靠爱护

罕用脱敏规定

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

良好的数据脱敏施行

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());
    }

}

测试后果


如图所示曾经进行脱敏

其余计划

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

正文完
 0