前言
什么是數(shù)據(jù)脫敏
數(shù)據(jù)脫敏是指對(duì)某些敏感信息通過脫敏規(guī)則進(jìn)行數(shù)據(jù)的變形,實(shí)現(xiàn)敏感隱私數(shù)據(jù)的可靠保護(hù)
常用脫敏規(guī)則
替換、重排、加密、截?cái)唷⒀诖a
良好的數(shù)據(jù)脫敏實(shí)施
1、盡可能地為脫敏后的應(yīng)用,保留脫敏前的有意義信息
2、最大程度地防止黑客進(jìn)行破解
今天我們聊聊如何自定義數(shù)據(jù)脫敏
整體思路
本示例通過替換的手段實(shí)現(xiàn)脫敏,然后配合常用的框架特性,比如mybatis的攔截器機(jī)制或者json的序列化來快速實(shí)現(xiàn)脫敏
具體落地
1、定義一個(gè)脫敏工具類
可以直接引用hutool工具包,不過它在5.6+版本以上才提供了這個(gè)工具
https://www.hutool.cn/docs/#/core/工具類/信息脫敏工具-DesensitizedUtil
不然就自己實(shí)現(xiàn)一個(gè),形如下
public class DesensitizedUtils {
/**
* 脫敏,使用默認(rèn)的脫敏策略
* <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("北京市海淀區(qū)馬連洼街道289號(hào)", DesensitizedUtils.DesensitizedType.ADDRESS)) = "北京市海淀區(qū)馬********"
* DesensitizedUtil.desensitized("[email protected]", 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、中文名、身份證號(hào)、座機(jī)號(hào)、手機(jī)號(hào)、地址、電子郵件、密碼
* @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】不對(duì)外提供userId
*
* @return 脫敏后的主鍵
*/
public static Long userId() {
return 0L;
}
/**
* 【中文姓名】只顯示第一個(gè)漢字,其他隱藏為2個(gè)星號(hào),比如:李**
*
* @param fullName 姓名
* @return 脫敏后的姓名
*/
public static String chineseName(String fullName) {
if (StrUtil.isBlank(fullName)) {
return StrUtil.EMPTY;
}
return StrUtil.hide(fullName, 1, fullName.length());
}
/**
* 【身份證號(hào)】前1位 和后2位
*
* @param idCardNum 身份證
* @param front 保留:前面的front位數(shù);從1開始
* @param end 保留:后面的end位數(shù);從1開始
* @return 脫敏后的身份證
*/
public static String idCardNum(String idCardNum, int front, int end) {
//身份證不能為空
if (StrUtil.isBlank(idCardNum)) {
return StrUtil.EMPTY;
}
//需要截取的長(zhǎng)度不能大于身份證號(hào)長(zhǎng)度
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);
}
/**
* 【手機(jī)號(hào)碼】前三位,后4位,其他隱藏,比如135****2210
*
* @param num 移動(dòng)電話;
* @return 脫敏后的移動(dòng)電話;
*/
public static String mobilePhone(String num) {
if (StrUtil.isBlank(num)) {
return StrUtil.EMPTY;
}
return StrUtil.hide(num, 3, num.length() - 4);
}
/**
* 【地址】只顯示到地區(qū),不顯示詳細(xì)地址,比如:北京市海淀區(qū)****
*
* @param address 家庭住址
* @param sensitiveSize 敏感信息長(zhǎng)度
* @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);
}
/**
* 【電子郵箱】郵箱前綴僅顯示第一個(gè)字母,前綴其他隱藏,用星號(hào)代替,@及后面的地址顯示,比如: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());
}
/**
* 【中國(guó)車牌】車牌中間用*代替
* eg1:null -》 ""
* eg1:"" -》 ""
* eg3:蘇D40000 -》 蘇D4***0
* eg4:陜A12345D -》 陜A1****D
* eg5:京A123 -》 京A123 如果是錯(cuò)誤的車牌,不處理
*
* @param carLicense 完整的車牌號(hào)
* @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;
}
/**
* 銀行卡號(hào)脫敏
* eg: 1101 **** **** **** 3256
*
* @param bankCardNo 銀行卡號(hào)
* @return 脫敏之后的銀行卡號(hào)
* @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();
}
}
其實(shí)正常到這個(gè)步驟,通過替換實(shí)現(xiàn)脫敏就可以完成,可以直接在程序中,直接調(diào)用這個(gè)工具就行。但是作為一個(gè)懂得偷懶的程序員,肯定不滿足這樣。于是我們會(huì)進(jìn)一步封裝
2、自定義脫敏注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sensitive {
DesensitizedType strategy() default DesensitizedType.NONE;
/**
* 是否使用dfa算法
* @return
*/
boolean useDFA() default false;
/**
* dfa敏感字符替換,默認(rèn)替換成 "*"
* @return
*/
String dfaReplaceChar() default "*";
/**
* dfa敏感字符替換次數(shù)
* @return
*/
int dfaReplaceCharRepeatCount() default 1;
}
3、利用一些框架特性提升效率
a、如果項(xiàng)目已經(jīng)有用mybatis,則可以利用mybatis攔截器特性。實(shí)現(xiàn)原理就是攔截響應(yīng)回來的結(jié)果,然后對(duì)結(jié)果進(jìn)行脫敏處理
@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、 如果項(xiàng)目是基于springboot的web項(xiàng)目,則可以利用springboot自帶的jackson自定義序列化實(shí)現(xiàn)。它的實(shí)現(xiàn)原來其實(shí)就是在json進(jìn)行序列化渲染給前端時(shí),進(jìn)行脫敏。
如果是這種方案,則需對(duì)自定義注解進(jìn)行改造一下,加上
@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敏感字符替換,默認(rèn)替換成 "*"
* @return
*/
String dfaReplaceChar() default "*";
/**
* dfa敏感字符替換次數(shù)
* @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、定義實(shí)體對(duì)象,需要進(jìn)行脫敏的屬性上加上脫敏注解
@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、編寫一個(gè)測(cè)試controller
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping(value="/list")
public AjaxResult listUsers(){
return AjaxResult.success(userService.listUserDTO());
}
}
測(cè)試結(jié)果
如圖所示已經(jīng)進(jìn)行脫敏
其他方案
1、基于Sharding Sphere實(shí)現(xiàn)數(shù)據(jù)脫敏
具體實(shí)現(xiàn)可以參考如下文章
https://jaskey.github.io/blog/2020/03/18/sharding-sphere-data-desensitization/
2、自定義注解格式化
主要實(shí)現(xiàn)步驟如下
- 1、實(shí)現(xiàn)AnnotationFormatterFactory接口
- 2、創(chuàng)建脫敏格式化類實(shí)現(xiàn)Formatter
- 3、將AnnotationFormatterFactory實(shí)現(xiàn)的接口注冊(cè)到FormatterRegistry
具體實(shí)現(xiàn)可以參考如下文章
https://blog.csdn.net/qq_27081015/article/details/103295983
4、利用fastjson進(jìn)行脫敏
主要實(shí)現(xiàn)步驟如下
- 1、實(shí)現(xiàn)ValueFilter接口,在process進(jìn)行脫敏
- 2、配置fastjson為默認(rèn)JSON轉(zhuǎn)換
/**
* 配置fastjson為默認(rèn)JSON轉(zhuǎn)換
*
* @return
*/
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
// 1.定義一個(gè)converters轉(zhuǎn)換消息的對(duì)象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
// 2.添加fastjson的配置信息,比如: 是否需要格式化返回的json數(shù)據(jù)
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對(duì)象
return new HttpMessageConverters(converter);
}
具體實(shí)現(xiàn)可以參考如下文章
https://blog.csdn.net/qq_27081015/article/details/103297316
5、利用mybatis-mate
mybatis-plus 企業(yè)(數(shù)據(jù)優(yōu)雅處理)模塊,使用時(shí)要配置一下授權(quán)碼。如下
mybatis-mate:
cert:
grant: jinTianYiXueKe
license: GKXP9r4MCJhGID/DTGigcBcLmZjb1YZGjE4GXaAoxbtGsPC20sxpEtiUr2F7Nb1ANTUekvF6Syo6DzraA4M4oacwoLVTglzfvaEyUogW8L7mydqlsZ4+hlm20kK85eLJK1QsskrSJmreMnEaNh9lsV7Lpbxy9JeGCeM0HPEbRvq8Y+8dUt5bQYLklsa3ZIBexir+4XykZY15uqn1pYIp4pEK0+aINTa57xjJNoWuBIqm7BdFIb4l1TAcPYMTsMXhF5hfMmKD2h391HxWTshJ6jbt4YqdKD167AgeoM+B+DE1jxlLjcpskY+kFs9pIOS7RCcmKBBUOgX2BD/JxhR2gQ==
他的實(shí)現(xiàn)機(jī)理就是利用json序列化那種,如果感興趣可以參考如下鏈接
https://gitee.com/baomidou/mybatis-mate-examples
本文的demo也有基于mybatis-mate實(shí)現(xiàn)脫敏,鏈接如下
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-desensitization/springboot-desensitzation-mybatis-mate
總結(jié)
有時(shí)候業(yè)務(wù)場(chǎng)景的實(shí)現(xiàn)方式有多種多樣,大家要懂得取舍判斷,比如上面的方案如果你的項(xiàng)目本來就沒用mybatis,但為了脫敏又引入mybatis,這種方案就額外有加入了復(fù)雜度,后面維護(hù)估計(jì)就有得折騰了
demo鏈接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-desensitization






