前言
什么是数据脱敏
数据脱敏是指对某些敏感信息通过脱敏规定进行数据的变形,实现敏感隐衷数据的牢靠爱护
罕用脱敏规定
替换、重排、加密、截断、掩码
良好的数据脱敏施行
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)@Documentedpublic @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@Builderpublic 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