本文已参与「新人创作礼」活动,一起开启掘金创作之路。
业务背景
大数据时代的到来,颠覆了传统业态的运作模式,激发出新的生产潜能。数据成为重要的生产要素,是信息的载体,数据间的流动也潜藏着更高阶维度的价值信息。数据脱敏(Data Masking),顾名思义,是屏蔽敏感数据,对某些敏感信息(比如,身份证号、手机号、卡号、客户姓名、客户地址、邮箱地址、薪资等等 )通过脱敏规则进行数据的变形,实现隐私数据的可靠保护。
方案分析
由于业务要求需要把部分功能所展示的数据进行脱敏处理,对于这一需求,讨论了以下几种方案:
- 在数据库层做数据迁移的时候,就将对应的值进行脱敏处理,系统根据其他条件直接查询就可以;但是这样做会出现数据同步不及时,或者报错,两个库的数据不一致,不可取;
- 在Dao层对需要脱敏的字段进行sql拼接处理,返回页面;这样做效率太低且sql不好写,业务功能量少的情况下可以使用;
- 系统使用注解以及工具类,进行全局脱敏;使用起来方便且代码会很规范,可取;
实现思路
- 自定义注解,且仅能作用在字段中,在方法返回使用到的DTO中,根据业务需要,分别在对应字段上加入此注解,以及数据脱敏类型(此处用到了枚举);
import com.wd.basic.common.desensitization.enums.SensitiveTypeEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 脱敏注解
*
* @author 上官婉儿
* @date 2022/03/24
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)//字段、枚举的常量
public @interface Desensitize {
/**
* 脱敏字段类型
*
* @return {@link SensitiveTypeEnum}
*/
SensitiveTypeEnum type() default SensitiveTypeEnum.REGULAR_EXPRESSION;
/**
* 正则表达式
* @return
*/
String pattern() default "";
}
package com.wd.basic.common.desensitization.enums;
import com.wd.basic.common.pojo.base.enums.IEnum;
import lombok.Getter;
/**
* 脱敏数据类型
*/
public enum SensitiveTypeEnum implements IEnum {
REGULAR_EXPRESSION("REGULAR_EXPRESSION", ""),
CHINESE_NAME("CHINESE_NAME", "中文名"),
PHONE("PHONE", "手机号"),
ADRESS("ADRESS", "地址"),
CERNO("CERNO", "身份证号"),
EMAIL("EMAIL", "邮箱"),
LANDLINE("LANDLINE", "座机");
/**
* 代码
*/
@Getter
private String code;
@Getter
private String desc;
SensitiveTypeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}
2.自定义工具类,根据传入的对象,一层层解析得到每个需要进行脱敏的字段,再根据注解中传入的数据脱敏类型,调用不同的脱敏规则方法,最后将对象返回,降低代码耦合度;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.google.common.collect.Lists;
import com.wd.basic.common.desensitization.comment.Desensitize;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 敏感数据脱敏处理工具类
*
* @author 上官婉儿
* @date 2021/1/26
*/
@Slf4j
public class DesensitizedUtils {
/**
* 递归深度计数器:防止循环依赖
*/
private static ThreadLocal<AtomicInteger> threadLocalCounter = new ThreadLocal<>();
/**
* 递归的最大深度
*/
private static final Integer MAX_DEPTH = 3;
/**
* 敏感数据脱敏
* 脱敏对象 的字段 需要添加 {@Sensitive} 注解并标注敏感类型
*
* @param bean
* @return
*/
public static Object desensitization(Object bean) {
try {
// 控制递归的深度:防止循环依赖
threadLocalCounter.set(new AtomicInteger(MAX_DEPTH));
doDesensitization(bean);
} catch (Exception e) {
log.warn("数据脱敏工具异常", e);
} finally {
threadLocalCounter.remove();
}
return bean;
}
/**
* 遍历递归脱敏操作
*
* @param bean
* @throws Exception
*/
private static void doDesensitization(Object bean) throws Exception {
if (Objects.isNull(bean) || isBeyoudMaxDepth(threadLocalCounter.get())) {
return;
}
//处理子属性,包括集合中的
Object object = null;
if (bean.getClass().isArray()) {
//对数组类型的字段进行递归过滤
for (int i = 0; i < Array.getLength(bean); i++) {
object = Array.get(bean, i);
if (isNotGeneralType(object.getClass())) {
desensitizationField(object);
}
}
} else if (bean instanceof Collection<?>) {
//对集合类型的字段进行递归过滤
Iterator<?> it = ((Collection<?>) bean).iterator();
while (it.hasNext()) {
object = it.next();
if (isNotGeneralType(object.getClass())) {
desensitizationField(object);
}
}
} else if (bean instanceof Map<?, ?>) {
//对Map类型的字段进行递归过滤
Map<?, ?> map = (Map<?, ?>) bean;
for (Object o : map.entrySet()) {
object = ((Map.Entry<?, ?>) o).getValue();
if (isNotGeneralType(object.getClass())) {
desensitizationField(object);
}
}
} else {//自定义类等
if (isNotGeneralType(bean.getClass())) {
desensitizationField(bean);
}
}
}
/**
* 脱敏bean所有属性
*
* @param bean
* @throws Exception
*/
private static void desensitizationField(Object bean) throws Exception {
// 获取对象的所有属性
List<Field> fields = getAllFields(bean);
if (CollectionUtils.isEmpty(fields)) {
return;
}
Object value;
for (Field field : fields) {
field.setAccessible(true);
value = field.get(bean);
// 属性是final类型 不处理
if (Objects.isNull(value)) {
continue;
}
//不是基本数据类型
if (isNotGeneralType(value.getClass())) {
doDesensitization(value);
} else {
//脱敏操作
setNewValueForField(bean, field, value);
}
}
}
/**
* 获取包括父类所有 使用了{@Desensitize}注解的属性
*
* @param objSource
* @return
*/
private static List<Field> getAllFields(Object objSource) {
if (Objects.isNull(objSource)) {
return null;
}
AtomicInteger atomicInteger = new AtomicInteger(MAX_DEPTH);
// 获得当前类的所有属性(private、protected、public)
List<Field> fieldList = Lists.newArrayList();
Class tempClass = objSource.getClass();
while (Objects.nonNull(tempClass) && !tempClass.getName().toLowerCase().equals("java.lang.object") && !isBeyoudMaxDepth(atomicInteger)) {
fieldList.addAll(Arrays.asList(tempClass.getDeclaredFields()));
//得到父类,然后赋给自己
tempClass = tempClass.getSuperclass();
}
// 过滤出来需要
return fieldList.stream().filter(field -> !Modifier.isFinal(field.getModifiers()) && Objects.nonNull(field.getAnnotation(Desensitize.class))).collect(Collectors.toList());
}
/**
* 排除基础类型、枚举类型、扩展库类型的字段
*
* @param clazz
* @return
*/
private static boolean isNotGeneralType(Class<?> clazz) {
return !clazz.isPrimitive()
&& !clazz.isEnum()
&& Objects.nonNull(clazz.getPackage())
&& clazz.getPackage().getName().indexOf("javax.") == -1
&& clazz.getPackage().getName().indexOf("java.lang") == -1
&& clazz.getPackage().getName().indexOf("java.time") == -1;
}
/**
* 脱敏操作(按照规则转化需要脱敏的字段并设置新值)
* 目前只支持String类型的字段
*
* @param bean
* @param field
* @param value
* @throws IllegalAccessException
*/
public static void setNewValueForField(Object bean, Field field, Object value) {
try {
//处理自身的属性
Desensitize annotation = field.getAnnotation(Desensitize.class);
String valueStr;
if (!field.getType().equals(String.class) || Objects.isNull(value)
|| ObjectUtils.isEmpty(valueStr = value.toString()) || Objects.isNull(annotation)) {
return;
}
switch (annotation.type()) {
case CHINESE_NAME: {
field.set(bean, chineseName(valueStr));
break;
}
case PHONE: {
field.set(bean, phone(valueStr));
break;
}
case ADRESS: {
field.set(bean, adress(valueStr));
break;
}
case CERNO: {
field.set(bean, cerno(valueStr));
break;
}
case EMAIL: {
field.set(bean, email(valueStr));
break;
}
case LANDLINE: {
field.set(bean, landline(valueStr));
}
case REGULAR_EXPRESSION: {
field.set(bean, regReplace(valueStr, annotation.pattern()));
break;
}
default:
field.set(bean, defaultDesensitized(valueStr));
break;
}
} catch (Exception e) {
// log.warn("数据脱敏工具异常,bean:{},field:{},value:{}", GsonUtils.obj2Json(bean), field.getName(), value, e);
}
}
private static Boolean isBeyoudMaxDepth(AtomicInteger atomicInteger) {
if (Objects.isNull(atomicInteger)) {
return Boolean.FALSE;
}
final int increment = atomicInteger.getAndDecrement();
if (increment <= 0) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
public static String defaultDesensitized(String value) {
if (Objects.isNull(value)) {
return null;
}
return value.replaceAll(value, StrUtil.fillBefore("*", '*', value.length()));
}
/**
* 【姓名】真实姓名脱敏
* 中文姓名不显示中间的文字,其他隐藏为星号
* 张三丰 :张*丰
*
* @param value
* @return
*/
public static String chineseName(String value) {
if (Objects.isNull(value)) {
return null;
}
if (value.length() == 1) {
return value;
} else if (value.length() == 2) {
return value.substring(0) + "*";
} else {
return value.replaceAll(value.substring(1, value.length() - 1),
StrUtil.fillBefore("*", '*', value.length() - 2));
}
}
/**
* 手机号脱敏规则
*
* @param value
* @return
*/
public static String phone(String value) {
if (Objects.isNull(value)) {
return null;
}
return value.replaceAll("(\w{3})\w*(\w{4})", "$1****$2");
}
/**
* 身份证号脱敏规则 : 保留前六后三 适用于15位和18位身份证号
*
* @param value
* @return
*/
public static String cerno(String value) {
if (!Objects.isNull(value)) {
if (value.length() == 15) {
value = value.replaceAll("(\w{6})\w*(\w{3})", "$1******$2");
}
if (value.length() == 18) {
value = value.replaceAll("(\w{6})\w*(\w{3})", "$1*********$2");
}
}
return value;
}
/**
* 座机:
* 没有 "-" 前3位 + 后2位
* 有 “-” 后前3位 + 后2位
*
* @param value
* @return
*/
public static String landline(String value) {
if (Objects.isNull(value)) {
return null;
}
int s = value.indexOf("-");
if (s == -1) {
return value.replaceAll(value.substring(2, value.length() - 2), StrUtil.fillBefore("*", '*', value.length() - 5));
}
return value.replaceAll(value.substring(s + 3, value.length() - 2), StrUtil.fillBefore("*", '*', value.length() - s - 5));
}
/**
* 邮箱规则
*
* @param value
* @return
* @前数字 前两位+*。。。*+后两位
*/
public static String email(String value) {
if (Objects.isNull(value)) {
return null;
}
String regExp = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$";
Pattern p = Pattern.compile(regExp);
Matcher matcher = p.matcher(value);
boolean isMatched = matcher.matches();
if (isMatched) {
int s = value.indexOf("@");
return value.replaceAll(value.substring(2, s - 2), StrUtil.fillBefore("*", '*', s - 4));
} else {
return value.replaceAll(value, StrUtil.fillBefore("*", '*', value.length()));
}
}
/**
* 地址脱敏规则:
* 3位以下全是 *
* 3-7 前三位+ *。。。*
* 其余 前三位+ *。。。* + 后三位
*
* @param value
* @return
*/
public static String adress(String value) {
if (Objects.isNull(value)) {
return null;
}
if (value.length() <= 3) {
return "***";
} else if (value.length() > 3 && value.length() <= 7) {
return value.substring(0, 2) + StrUtil.fillBefore("*", '*', value.length() - 3);
} else {
return value.replaceAll(value.substring(3, value.length() - 3), StrUtil.fillBefore("*", '*', value.length() - 6));
}
}
/**
* 正则表达式字符串替换
*
* @param content 字符串
* @param pattern 正则表达式
* @return 返回替换后的字符串
*/
public static String regReplace(String content, String pattern) {
if (Objects.isNull(pattern)) {
return content;
}
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(content);
String result = m.replaceAll("*");
return result;
}
}
- 实现根据枚举类中的脱敏数据类型,使用不同的数据脱敏处理方法;若在枚举中新增数据脱敏类型,需要在DesensitizedUtils.setNewValueForField()中的 switch()循环中新增case值,以及在此处调用的该数据脱敏类型的对应处理规则方法;
- 新增切面实现系统全局脱敏处理,且控制到service业务逻辑层;在系统内配置文件中新增 参数: desensitized.isOpen:true;可以灵活控制是否开启此功能;
@Component
public class DesensitizedConfig {
@Value(value = "${desensitized.isOpen:true}")
private Boolean isOpen;
/**
* 切入点
*/
@Pointcut("execution(* com..*.service..*.*(..))")
public void pointcut() {
}
/**
* @param joinPoint 连接点
* @return {@link Object}
* @throws Throwable throwable
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object obj = joinPoint.proceed();//执行方法,获取返回值
if(isOpen){
DesensitizedUtils.desensitization(obj);//脱敏处理
}
return obj;
}
}
使用规则
- 在系统内配置文件中设置参数: desensitized.isOpen:true;
- 加入切面控制类DesensitizedConfig;@Pointcut("execution(在service实现层)");
- 在方法返回使用到的DTO中,根据业务需要,分别在对应字段上加入注解@Desensitize(), 属性type:数据脱敏类型SensitiveTypeEnum();属性pattern:正则表达式pattern(),默认为空;
4.现系统提供姓名、身份证号、手机号、地址、邮箱、座机的字段脱敏处理;若在枚举中新增数据脱敏类型,需要在DesensitizedUtils.setNewValueForField()中的 switch()循环中新增case值,以及在此处调用的该数据脱敏类型的对应处理规则方法;
- 当type=SensitiveTypeEnum.REGULAR_EXPRESSION时,注解中需传入pattern = “正则表达式“,规则为:Pattern.compile(pattern).matcher(value)..replaceAll("*");
测试
根据使用规则,将demo实体类中 demo_code和demo_name两个字段加上了该自定义注解,但不同的类型,调用接口测试结果如下: