⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7225848846785544247 「是心无吖」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

1 起因

最近在写一个功能,对用户敏感的数据进行脱敏,在网上看一圈基本上都是全局范围的,我觉得应该更加灵活,在不同场景,不同业务下进行脱敏更加合适。

JsonSerializer介绍就参考这位大佬的

https://juejin.cn/post/6872636051237240846

aop介绍参考这位大佬的

https://juejin.cn/post/6844903575441637390

2 初步尝试

枚举类

/**
* 敏感信息枚举类
*
**/
public enum PrivacyTypeEnum {

/**
* 自定义
*/
CUSTOMER,
/**
* 用户名, 张*三, 李*
*/
CHINESE_NAME,
/**
* 身份证号, 110110********1234
*/
ID_CARD,
/**
* 座机号, ****1234
*/
FIXED_PHONE,
/**
* 手机号, 176****1234
*/
MOBILE_PHONE,
/**
* 地址, 北京********
*/
ADDRESS,
/**
* 电子邮件, s*****o@xx.com
*/
EMAIL,
/**
* 银行卡, 622202************1234
*/
BANK_CARD,
/**
* 密码, 永远是 ******, 与长度无关
*/
PASSWORD,
/**
* 密钥, 永远是 ******, 与长度无关
*/
KEY

}

注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})//作用于字段上
@JacksonAnnotationsInside // 表示自定义自己的注解PrivacyEncrypt
@JsonSerialize(using = PrivacySerialize.class)// 该注解使用序列化的方式
public @interface PrivacyEncrypt {

/**
* 脱敏数据类型, 非Customer时, 将忽略 refixNoMaskLen 和 suffixNoMaskLen 和 maskStr
*/
PrivacyTypeEnum type() default PrivacyTypeEnum.CUSTOMER;

/**
* 前置不需要打码的长度
*/
int prefixNoMaskLen() default 0;

/**
* 后置不需要打码的长度
*/
int suffixNoMaskLen() default 0;

/**
* 用什么打码
*/
String maskStr() default "*";

}

序列化类

public class PrivacySerialize extends JsonSerializer<String> implements ContextualSerializer {
public static final Logger logger = LoggerFactory.getLogger(PrivacySerialize.class);
private PrivacyTypeEnum type;

private Integer prefixNoMaskLen;

private Integer suffixNoMaskLen;

private String maskStr;

public PrivacySerialize(PrivacyTypeEnum type, Integer prefixNoMaskLen, Integer suffixNoMaskLen, String maskStr) {
this.type = type;
this.prefixNoMaskLen = prefixNoMaskLen;
this.suffixNoMaskLen = suffixNoMaskLen;
this.maskStr = maskStr;
}
public PrivacySerialize() {
}

@Override
public void serialize(String origin,JsonGenerator jsonGenerator,SerializerProvider serializerProvider) throws IOException {
if (StringUtils.isNotBlank(origin) && null != type) {
switch (type) {
case CHINESE_NAME:
jsonGenerator.writeString(DesensitizedUtils.chineseName(origin));
break;
case ID_CARD:
jsonGenerator.writeString(DesensitizedUtils.idCardNum(origin));
break;
case FIXED_PHONE:
jsonGenerator.writeString(DesensitizedUtils.fixedPhone(origin));
break;
case MOBILE_PHONE:
jsonGenerator.writeString(DesensitizedUtils.mobilePhone(origin));
break;
case ADDRESS:
jsonGenerator.writeString(DesensitizedUtils.address(origin));
break;
case EMAIL:
jsonGenerator.writeString(DesensitizedUtils.email(origin));
break;
case BANK_CARD:
jsonGenerator.writeString(DesensitizedUtils.bankCard(origin));
break;
case PASSWORD:
jsonGenerator.writeString(DesensitizedUtils.password(origin));
break;
case KEY:
jsonGenerator.writeString(DesensitizedUtils.key(origin));
break;
case CUSTOMER:
jsonGenerator.writeString(DesensitizedUtils.desValue(origin, prefixNoMaskLen, suffixNoMaskLen, maskStr));
break;
default:
throw new IllegalArgumentException("Unknow sensitive type enum " + type);
}
}else {
jsonGenerator.writeString("");
}

}

@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider,BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
PrivacyEncrypt encrypt = beanProperty.getAnnotation(PrivacyEncrypt.class);
if (encrypt == null) {
encrypt = beanProperty.getContextAnnotation(PrivacyEncrypt.class);
}
if (encrypt != null) {
return new PrivacySerialize(encrypt.type(), encrypt.prefixNoMaskLen(),
encrypt.suffixNoMaskLen(), encrypt.maskStr());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(null);
}
}

脱敏工具类

/**
* 脱敏工具类
*
**/
public class DesensitizedUtils {

/**
* 对字符串进行脱敏操作
* @param origin 原始字符串
* @param prefixNoMaskLen 左侧需要保留几位明文字段
* @param suffixNoMaskLen 右侧需要保留几位明文字段
* @param maskStr 用于遮罩的字符串, 如'*'
* @return 脱敏后结果
*/
public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {
if (origin == null) {
return null;
}

StringBuilder sb = new StringBuilder();
for (int i = 0, n = origin.length(); i < n; i++) {
if (i < prefixNoMaskLen) {
sb.append(origin.charAt(i));
continue;
}
if (i > (n - suffixNoMaskLen - 1)) {
sb.append(origin.charAt(i));
continue;
}
sb.append(maskStr);
}
return sb.toString();
}

/**
* 【中文姓名】只显示最后一个汉字,其他隐藏为星号,比如:**梦
* @param fullName 姓名
* @return 结果
*/
public static String chineseName(String fullName) {
if (fullName == null) {
return null;
}
return desValue(fullName, 0, 1, "*");
}

/**
* 【身份证号】显示前六位, 四位,其他隐藏。共计18位或者15位,比如:340304*******1234
* @param id 身份证号码
* @return 结果
*/
public static String idCardNum(String id) {
return desValue(id, 6, 4, "*");
}

/**
* 【固定电话】后四位,其他隐藏,比如 ****1234
* @param num 固定电话
* @return 结果
*/
public static String fixedPhone(String num) {
return desValue(num, 0, 4, "*");
}

/**
* 【手机号码】前三位,后四位,其他隐藏,比如135****6810
* @param num 手机号码
* @return 结果
*/
public static String mobilePhone(String num) {
return desValue(num, 3, 4, "*");
}

/**
* 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****
* @param address 地址
* @return 结果
*/
public static String address(String address) {
return desValue(address, 6, 0, "*");
}

/**
* 【电子邮箱 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com
* @param email 电子邮箱
* @return 结果
*/
public static String email(String email) {
return email.replaceAll("(\w?)(\w+)(\w)(@\w+\.[a-z]+(\.[a-z]+)?)", "$1****$3$4");

}

/**
* 【银行卡号】前六位,后四位,其他用星号隐藏每位1个星号,比如:622260**********1234
* @param cardNum 银行卡号
* @return 结果
*/
public static String bankCard(String cardNum) {
return desValue(cardNum, 6, 4, "*");
}

/**
* 【密码】密码的全部字符都用*代替,比如:******
* @param password 密码
* @return 结果
*/
public static String password(String password) {
if (password == null) {
return null;
}
return "******";
}

/**
* 【密钥】密钥除了最后三位,全部都用*代替,比如:***xdS 脱敏后长度为6,如果明文长度不足三位,则按实际长度显示,剩余位置补*
* @param key 密钥
* @return 结果
*/
public static String key(String key) {
if (key == null) {
return null;
}
int viewLength = 6;
StringBuilder tmpKey = new StringBuilder(desValue(key, 0, 3, "*"));
if (tmpKey.length() > viewLength) {
return tmpKey.substring(tmpKey.length() - viewLength);
}
else if (tmpKey.length() < viewLength) {
int buffLength = viewLength - tmpKey.length();
for (int i = 0; i < buffLength; i++) {
tmpKey.insert(0, "*");
}
return tmpKey.toString();
}
else {
return tmpKey.toString();
}
}

}

注解使用

的确实现了数据脱敏,但是有个问题现在的脱敏针对的是 只要对该实体类进行了使用返回的接口,中的数据都会进行脱敏,在有些场景下是不需要的,所以说要进行改进。

3 第二版改进

我的思路是在该实体类中在继承一个 父类其中定义一个字段,使其作为是否进行脱敏的开关,并且该实体类字段不参与序列化 脱敏控制类

public class DataMaskKey implements Serializable {
//不进行序列化,设置key 来进行过滤的把控,默认不开启
private transient Boolean isPrivacyKey = false;

public Boolean getPrivacyKey() {
return isPrivacyKey;
}

public void setPrivacyKey(Boolean privacyKey) {
isPrivacyKey = privacyKey;
}
}

更新之后的序列化类

思路就是通过反射获取,该成员的属性,因为不知道会继承多少,所以要进行递归查找需要的字段

public class PrivacySerialize extends JsonSerializer<String> implements ContextualSerializer {
public static final Logger logger = LoggerFactory.getLogger(PrivacySerialize.class);
private PrivacyTypeEnum type;

private Integer prefixNoMaskLen;

private Integer suffixNoMaskLen;

private String maskStr;

public PrivacySerialize(PrivacyTypeEnum type, Integer prefixNoMaskLen, Integer suffixNoMaskLen, String maskStr) {
this.type = type;
this.prefixNoMaskLen = prefixNoMaskLen;
this.suffixNoMaskLen = suffixNoMaskLen;
this.maskStr = maskStr;
}
public PrivacySerialize() {
}

@Override
public void serialize(String origin,JsonGenerator jsonGenerator,SerializerProvider serializerProvider) throws IOException {
boolean flag = false;
//反射获取对象
Object currentValue = jsonGenerator.getOutputContext().getCurrentValue();
//反射获取class
Class<?> aClass = jsonGenerator.getOutputContext().getCurrentValue().getClass();
List<Field> fieldList = getFieldList(aClass);
for (Field field : fieldList) {
//开始反射获取
String name = field.getName();
if ("isPrivacyKey".equals(name)){
try {
//进行重新赋值
flag = (boolean) field.get(currentValue);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
//反射进行 进行开关判定
if (flag){
//logger.info("进行脱敏处理");
if (StringUtils.isNotBlank(origin) && null != type) {
switch (type) {
case CHINESE_NAME:
jsonGenerator.writeString(DesensitizedUtils.chineseName(origin));
break;
case ID_CARD:
jsonGenerator.writeString(DesensitizedUtils.idCardNum(origin));
break;
case FIXED_PHONE:
jsonGenerator.writeString(DesensitizedUtils.fixedPhone(origin));
break;
case MOBILE_PHONE:
jsonGenerator.writeString(DesensitizedUtils.mobilePhone(origin));
break;
case ADDRESS:
jsonGenerator.writeString(DesensitizedUtils.address(origin));
break;
case EMAIL:
jsonGenerator.writeString(DesensitizedUtils.email(origin));
break;
case BANK_CARD:
jsonGenerator.writeString(DesensitizedUtils.bankCard(origin));
break;
case PASSWORD:
jsonGenerator.writeString(DesensitizedUtils.password(origin));
break;
case KEY:
jsonGenerator.writeString(DesensitizedUtils.key(origin));
break;
case CUSTOMER:
jsonGenerator.writeString(DesensitizedUtils.desValue(origin, prefixNoMaskLen, suffixNoMaskLen, maskStr));
break;
default:
throw new IllegalArgumentException("Unknow sensitive type enum " + type);
}
}else {
jsonGenerator.writeString("");
}
}else {
//logger.info("不进行脱敏处理");
jsonGenerator.writeString(origin);
}

}

@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider,BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
PrivacyEncrypt encrypt = beanProperty.getAnnotation(PrivacyEncrypt.class);
if (encrypt == null) {
encrypt = beanProperty.getContextAnnotation(PrivacyEncrypt.class);
}
if (encrypt != null) {
return new PrivacySerialize(encrypt.type(), encrypt.prefixNoMaskLen(),
encrypt.suffixNoMaskLen(), encrypt.maskStr());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(null);
}

private List<Field> getFieldList(Class<?> clazz){
if(null == clazz){
return null;
}
List<Field> fieldList = new ArrayList<>();
//递归查找 需求 的字段
Class<?> aClass = ClassRecursionUtils.getClass(clazz, "isPrivacyKey");
Field[] declaredFields = aClass.getDeclaredFields();
for (Field field : declaredFields) {
if (field != null){
//设置属性的可访问性
field.setAccessible(true);
//过滤静态
if(Modifier.isStatic(field.getModifiers())){
continue;
}
String name = field.getName();
//过滤非布尔类型
Class<?> type = field.getType();
//并且只添加 isPrivacyKey
if (type.isAssignableFrom(Boolean.class) && "isPrivacyKey".equals(name)){
fieldList.add(field);
}
}
}
return fieldList;
}

}

递归工具类

public class ClassRecursionUtils {
public static Class<?> getClass(Class<?> c, String fieldName) {
if (c !=null && !hasField(c, fieldName)) {
return getClass(c.getSuperclass(), fieldName);
}
return c;
}

public static boolean hasField(Class<?> c, String fieldName){
Field[] fields = c.getDeclaredFields();
for (Field f : fields) {
if (fieldName.equals(f.getName())) {
return true;
}
}
return false;
}
}

现在只需要在进行实体类 封装数据时,在进行手动set即可

4 最终方案

在上述情况下可以实现 手动控制是否在某些场景下的脱敏,但是需要对原来的代码进行修改,我觉得不友好,所以采用aop的形式进行控制

项目的返回类型基本上为两种

  • 实体类作为返回
  • 分页返回

aop注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})//作用于方法上
public @interface PrivacyKeyAnnotation {

/**
* 是否启用序列化脱敏 默认开启
*/
boolean isKey() default true;

/**
* 是否为PageInfo<?>(分页对象)
*/
boolean isPageKey() default false;
}

aop类

@Component
@Aspect
public class PrivacyKeyAspect {
public static final Logger logger = LoggerFactory.getLogger(PrivacyKeyAspect.class);

/**
* @Description: 环绕通知包含此注解的
* @param: ProceedingJoinPoint joinPoint
* @return: Object
*/
@Around(value = "@annotation("aop注解地址xxxxx")")
public Object repeatSub(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}

/**
* @Description: 后置通知
*/
@AfterReturning(value = "@annotation("aop注解地址")",returning = "result")
public void setPrivacyKeyType(JoinPoint joinPoint, Object result) throws Throwable {
//进行注解 值获取
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//是否开启脱敏
boolean flag = method.getDeclaredAnnotation(PrivacyKeyAnnotation.class).isKey();
//是否对分页进行脱敏
boolean status = method.getDeclaredAnnotation(PrivacyKeyAnnotation.class).isPageKey();
if (!status) {
//进行返回值反射
Class<?> aClass = ClassRecursionUtils.getClass(result.getClass(), "isPrivacyKey");
if (null != aClass) {
setFieldMethod(result, flag, aClass);
}
} else {
//反射分页page
//反射list类型
Parameter[] parameters = signature.getMethod().getParameters();
//泛型名称
String name = parameters[0].getName();
//泛型class
Class<?> type = parameters[0].getType();
//包名
String typeName = type.getName();
PropertyDescriptor[] ps = Introspector.getBeanInfo(result.getClass(), Object.class).getPropertyDescriptors();
for (PropertyDescriptor prop : ps) {
if (prop.getPropertyType().isAssignableFrom(List.class)) { //List 集合类型
Object obj = result.getClass().getMethod(prop.getReadMethod().getName()).invoke(result);
if(obj !=null){
List<?> listObj = (List<?>) obj;
for (Object next : listObj) {
Class<?> classObj = Class.forName(typeName);
//获取成员变量
Class<?> keyClass = ClassRecursionUtils.getClass(classObj, "isPrivacyKey");
setFieldMethod(next, flag, keyClass);
}
}

}
}
}
}

/**
* 内容填充
*/
private void setFieldMethod(Object result, boolean flag, Class<?> aClass) throws IllegalAccessException {
Field[] declaredFields = aClass.getDeclaredFields();
for (Field field : declaredFields) {
//设置属性的可访问性
field.setAccessible(true);
//只获取isPrivacyKey
String name = field.getName();
//过滤非布尔类型
Class<?> type = field.getType();
//并且只添加 isPrivacyKey
if (type.isAssignableFrom(Boolean.class) && "isPrivacyKey".equals(name)){
//重新写入
field.set(result,flag);
}
}
}
}

使用 在service implement类方法上写入

最后,另一种实现方式,可以参考:

https://juejin.cn/post/7242145254057410615

文章目录
  1. 1. 1 起因
  2. 2. 2 初步尝试
  3. 3. 3 第二版改进
  4. 4. 4 最终方案