前言
数据脱敏是一种数据保护技术,它可以对敏感数据进行处理,使其失去原始意义,从而保护数据隐私。通俗地说,就是将原始数据中敏感信息的部分替换或删除,例如:手机号码中的一部分数字替换为星号,或是姓名中的一些字母用其他字母代替等等。数据脱敏可以保护个人隐私,防止个人信息被泄露、滥用,同时也符合现代法律法规对个人信息保护的要求,是网络安全和信息保护中不可或缺的一环。
本文将通过使用注解完成对字段的自定义脱敏功能。
正文
本文将介绍2种脱敏的方式,分别为:注解方式脱敏
和lambda方式脱敏
注解脱敏
定义脱敏注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerialize.class)
public @interface Dm {
/**
* 脱敏信息类型
*/
DmTypeEnum type();
/**
* 前置不需要打码的长度
* 只有 {@link DmTypeEnum} 为 {@link DmTypeEnum#CUSTOMER} 时可用
*/
int prefixNoMaskLen() default 0;
/**
* 后置不需要打码的长度
* 只有 {@link DmTypeEnum} 为 {@link DmTypeEnum#CUSTOMER} 时可用
*/
int suffixNoMaskLen() default 0;
/**
* 用什么打码
* 只有 {@link DmTypeEnum} 为 {@link DmTypeEnum#CUSTOMER} 时可用
*/
char symbol() default '*';
}
type()
方法为可以支持实现的脱敏信息类型,通过使用hutool工具,可以看到它支持以下枚举,故拉取出来形成类型枚举:
public enum DmTypeEnum {
/**
* 用户自定义
*/
CUSTOMER,
/**
* 用户id
*/
BACK_ID,
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份证号
*/
ID_CARD,
/**
* 座机号
*/
FIXED_PHONE,
/**
* 手机号
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 电子邮件
*/
EMAIL,
/**
* 密码
*/
PASSWORD,
/**
* 中国大陆车牌,包含普通车辆、新能源车辆
*/
CAR_LICENSE,
/**
* 银行卡
*/
BANK_CARD
}
关键语句在@JsonSerialize(using = SensitiveSerialize.class)上
,@JsonSerialize
是一个注解,它可以用来指定 Java 对象序列化成 JSON 格式时的序列化规则。具体而言,它可以指定序列化时属性名的命名方式、日期格式化等等。它的作用是使得 Java 对象可以方便地转化为 JSON 格式,这对于 Web 开发、移动应用开发等场景非常有用,故此我们在这里自定义一个实现,并在注解中引用。
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class SensitiveSerialize<T> extends JsonSerializer<T> implements ContextualSerializer {
/**
* 脱敏类型
*/
private DmTypeEnum sensitiveTypeEnum;
/**
* 前几位不脱敏
*/
private Integer prefixNoMaskLen;
/**
* 最后几位不脱敏
*/
private Integer suffixNoMaskLen;
/**
* 打码字符
*/
private char symbol;
@Override
public void serialize(final T value, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException {
Object desensitized = DmUtils.desensitized(value.toString(), sensitiveTypeEnum, prefixNoMaskLen, suffixNoMaskLen, symbol);
if (desensitized instanceof String) {
jsonGenerator.writeString(desensitized.toString());
return;
}
if (desensitized instanceof Number) {
jsonGenerator.writeNumber(DmUtils.backId());
return;
}
log.error("[数据脱敏]未知的信息类型:{}", sensitiveTypeEnum);
jsonGenerator.writeString(StrUtil.EMPTY);
}
@Override
public JsonSerializer<?> createContextual(final SerializerProvider serializerProvider,
final BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
if (
Objects.equals(beanProperty.getType().getRawClass(), String.class)
|| Objects.equals(beanProperty.getType().getSuperClass().getRawClass(), Number.class)
) {
Dm sensitive = beanProperty.getAnnotation(Dm.class);
if (sensitive == null) {
sensitive = beanProperty.getContextAnnotation(Dm.class);
}
if (sensitive != null) {
return new SensitiveSerialize<T>(sensitive.type(), sensitive.prefixNoMaskLen(),
sensitive.suffixNoMaskLen(), sensitive.symbol());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(null);
}
}
脱敏工具类(对照hutool脱敏方法)
@Slf4j
public class DmUtils {
/**
* 脱敏,使用默认的脱敏策略
* <pre>
* desensitized("100", DesensitizedUtils.DesensitizedType.USER_ID)) = "0"
* desensitized("段正淳", DesensitizedUtils.DesensitizedType.CHINESE_NAME)) = "段**"
* desensitized("51343620000320711X", DesensitizedUtils.DesensitizedType.ID_CARD)) = "5***************1X"
* desensitized("09157518479", DesensitizedUtils.DesensitizedType.FIXED_PHONE)) = "0915*****79"
* desensitized("18049531999", DesensitizedUtils.DesensitizedType.MOBILE_PHONE)) = "180****1999"
* desensitized("北京市海淀区马连洼街道289号", DesensitizedUtils.DesensitizedType.ADDRESS)) = "北京市海淀区马********"
* desensitized("duandazhi-jack@gmail.com.cn", DesensitizedUtils.DesensitizedType.EMAIL)) = "d*************@gmail.com.cn"
* desensitized("1234567890", DesensitizedUtils.DesensitizedType.PASSWORD)) = "**********"
* desensitized("苏D40000", DesensitizedUtils.DesensitizedType.CAR_LICENSE)) = "苏D4***0"
* desensitized("11011111222233333256", DesensitizedUtils.DesensitizedType.BANK_CARD)) = "1101 **** **** **** 3256"
* </pre>
*
* @param message 字符串
* @param desensitizedType 脱敏类型;可以脱敏:用户id、中文名、身份证号、座机号、手机号、地址、电子邮件、密码
* @param startInclude 开始排除 类型为 CUSTOMER 时可用
* @param endExclude 结束排除 类型为 CUSTOMER 时可用
* @param symbol 脱敏字符 默认为'*' 类型为 CUSTOMER 时可用
* @return 脱敏之后的字符串
*/
public static Object desensitized(Object message, DmTypeEnum desensitizedType, int startInclude, int endExclude, char symbol) {
if (message instanceof Number && StrUtil.isBlank(message.toString())) {
return -1L;
}
if (StrUtil.isBlank(message.toString())) {
return StrUtil.EMPTY;
}
String newStr;
switch (desensitizedType) {
case CUSTOMER:
newStr = customer(String.valueOf(message), startInclude, endExclude, symbol);
break;
case BACK_ID:
log.info("[数据脱敏]后台用户id脱敏,值为:{}", message);
return backId();
case CHINESE_NAME:
newStr = chineseName(String.valueOf(message));
break;
case ID_CARD:
newStr = idCardNum(String.valueOf(message), 1, 2);
break;
case FIXED_PHONE:
newStr = fixedPhone(String.valueOf(message));
break;
case MOBILE_PHONE:
newStr = mobilePhone(String.valueOf(message));
break;
case ADDRESS:
newStr = address(String.valueOf(message), 8);
break;
case EMAIL:
newStr = email(String.valueOf(message));
break;
case PASSWORD:
newStr = password(String.valueOf(message));
break;
case CAR_LICENSE:
newStr = carLicense(String.valueOf(message));
break;
case BANK_CARD:
newStr = bankCard(String.valueOf(message));
break;
default:
newStr = "";
}
return newStr;
}
public static String replace(CharSequence str, int startInclude, int endExclude, char symbol) {
return CharSequenceUtil.replace(str, startInclude, endExclude, symbol);
}
public static String customer(String customer, int startInclude, int endExclude, char replacedChar) {
if (StrUtil.isBlank(customer)) {
return "";
}
if (startInclude > customer.length() || endExclude > customer.length() || (startInclude + endExclude) > customer.length()) {
log.warn("[信息脱敏]自定义长度不正确");
return replace(customer, startInclude, customer.length() - 2, replacedChar);
}
return replace(customer, startInclude, endExclude, replacedChar);
}
/**
* 【后台用户id】不对外提供backId
*
* @return 脱敏后的主键
*/
public static Long backId() {
return -1L;
}
/**
* 【中文姓名】只显示第一个汉字,其他隐藏为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 脱敏之后的银行卡号
*/
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();
}
}
lambda脱敏
注解方式脱敏的好处是可以在变量上更方便的进行脱敏,但是这种方式对数据格式支持不高,一般只支持基本的数据类型,无法对实体内嵌套的数据进行脱敏,故此可以通过lambda
方式进行脱敏。
首先自定义一个函数
public interface DmParam<Parent, Children> extends Serializable {
/**
* 执行函数
*
* @param parameter 参数
* @return 函数执行结果
* @throws Exception 自定义异常
*/
Children call(Parent parameter) throws Exception;
/**
* 执行函数,异常包装为RuntimeException
*
* @param parameter 参数
* @return 函数执行结果
*/
default Children callWithServiceException(Parent parameter) {
try {
return call(parameter);
} catch (Exception e) {
throw new ServiceException("信息脱敏失败");
}
}
}
既然通过lambda方式获取字段,那么反射也是必须的
public class FieldUtil {
/**
* 通过lambda获取字段
*
* @param func 方法
* @return {@link Field}
*/
public static <T> Field getField(DmParam<T, ?> func) {
FieldMeta fieldMeta = getFieldMeta(func);
return ReflectUtil.getField(fieldMeta.getGetInstantiatedClass(), fieldMeta.getPropertyName());
}
/**
* 通过lambda设置字段名称
*
* @param func 方法
* @param val 设置的值
*/
public static <T> void setField(Object object, DmParam<T, ?> func, String val) {
FieldMeta fieldMeta = getFieldMeta(func);
ReflectUtil.setFieldValue(object, fieldMeta.getPropertyName(), val);
}
/**
* 通过lambda获取字段名称
*
* @param func 方法
* @return {@link String}
*/
public static <T> FieldMeta getFieldMeta(DmParam<T, ?> func) {
// 反射读取
Method writeReplaceMethod;
try {
writeReplaceMethod = func.getClass().getDeclaredMethod("writeReplace");
// 设置可访问
writeReplaceMethod.setAccessible(true);
SerializedLambda serializedLambda;
serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(func);
String implMethodName = serializedLambda.getImplMethodName();
String propertyName = methodToProperty(implMethodName);
String clazzPath = serializedLambda.getImplClass().replace('/', '.');
Class<String> clazz = ClassUtil.getClass(clazzPath);
return new FieldMeta(propertyName, implMethodName, clazz);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.error("[FieldUtil]反射获取字段失败", e);
throw new ServiceException(e);
}
}
/**
* 通过lambda获取字段名称
*
* @param func 方法
* @return {@link String}
*/
public static <T> String getFieldName(DmParam<T, ?> func) {
// 反射读取
Method writeReplaceMethod;
try {
writeReplaceMethod = func.getClass().getDeclaredMethod("writeReplace");
// 设置可访问
writeReplaceMethod.setAccessible(true);
SerializedLambda serializedLambda;
serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(func);
String implMethodName = serializedLambda.getImplMethodName();
return methodToProperty(implMethodName);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.error("[FieldUtil]反射获取字段失败", e);
throw new ServiceException(e);
}
}
/**
* 根据方法名转换为字段
*
* @param implMethodName 方法名
* @return {@link String}
*/
public static String methodToProperty(String implMethodName) {
final String defaultIs = "is";
final String defaultGet = "get";
final String defaultSet = "set";
if (implMethodName.startsWith(defaultIs)) {
implMethodName = implMethodName.substring(2);
} else if (implMethodName.startsWith(defaultGet) || implMethodName.startsWith(defaultSet)) {
implMethodName = implMethodName.substring(3);
} else {
throw new ServiceException("Error parsing property name '" + implMethodName + "'. Didn't start with 'is', 'get' or 'set'.");
}
if (implMethodName.length() == 1 || (implMethodName.length() > 1 && !Character.isUpperCase(implMethodName.charAt(1)))) {
implMethodName = implMethodName.substring(0, 1).toLowerCase(Locale.ENGLISH) + implMethodName.substring(1);
}
return implMethodName;
}
}
定义脱敏方法:
对于单个实体
public class DmLambdaWrapper<T> {
T val;
@SuppressWarnings("rawtype")
public DmLambdaWrapper(T val) {
this.val = val;
}
/**
* 获取脱敏实例
*
* @param val 待脱敏的实体
* @return {@link DmLambdaWrapper<T>}
*/
public static <T> DmLambdaWrapper<T> getInstance(T val) {
return new DmLambdaWrapper<>(val);
}
/**
* 关联单个字段
*
* @param field 关联字段
* @return {@link DmLambdaWrapper<R>}
*/
public <R> DmLambdaWrapper<R> ivkOne(DmParam<T, R> field) {
R call = field.callWithServiceException(val);
return new DmLambdaWrapper<>(call);
}
/**
* 关联集合
*
* @param listMapper 关联集合字段
* @param predicate 脱敏预言
* @return {@link DmListLambdaWrapper}
*/
public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper, Predicate<C> predicate) {
S cs = listMapper.callWithServiceException(val);
List<C> collect = cs.stream().filter(predicate).collect(Collectors.toList());
return new DmListLambdaWrapper<>(collect);
}
/**
* 关联集合-无预言(集合内所有对应字段都将脱敏)
*
* @param listMapper 关联集合字段
* @return {@link DmListLambdaWrapper}
*/
public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper) {
S cs = listMapper.callWithServiceException(val);
return new DmListLambdaWrapper<>(cs);
}
/**
* 脱敏字符串
*
* @param dmType 脱敏类型
* @param mapper 脱敏字段
* @return {@link DmLambdaWrapper<T>}
*/
public <R extends String> DmLambdaWrapper<T> dmStr(DmTypeEnum dmType, DmParam<T, R> mapper) {
R call = mapper.callWithServiceException(val);
if (null != call) {
String desensitized = DmUtils.desensitized(call, dmType, 1, 3, '*').toString();
FieldUtil.setField(val, mapper, desensitized);
// 反射获取字段名称
}
return new DmLambdaWrapper<>(val);
}
/**
* 脱敏多个字段 相同脱敏类型多字段性能要优于 {@link DmLambdaWrapper#dmStr(DmTypeEnum, DmParam)}
*
* @param dmType 脱敏类型
* @param mappers 脱敏字段
* @return {@link DmLambdaWrapper<T>}
* @author Mc
* @date 2022/12/16 17:32
*/
@SafeVarargs
public final <R extends String> DmLambdaWrapper<T> dmStr(DmTypeEnum dmType, final DmParam<T, R>... mappers) {
for (DmParam<T, R> mapper : mappers) {
R call = mapper.callWithServiceException(val);
if (null != call) {
String desensitized = DmUtils.desensitized(call, dmType, 1, 3, '*').toString();
FieldUtil.setField(val, mapper, desensitized);
// 反射获取字段名称
}
}
return new DmLambdaWrapper<>(val);
}
}
对于List集合脱敏
@Slf4j
public class DmListLambdaWrapper<T extends Collection<F>, F> {
T listVal;
public DmListLambdaWrapper(T listVal) {
this.listVal = listVal;
}
/**
* 关联集合 带过滤
*
* @param listMapper 集合Mapper
* @param predicate 过滤预言
* @return {@link DmListLambdaWrapper}
* @author Mc
* @date 2022/12/16 17:24
*/
public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper, Predicate<C> predicate) {
S cs = listMapper.callWithServiceException(listVal);
List<C> collect = cs.stream().filter(predicate).collect(Collectors.toList());
return new DmListLambdaWrapper<>(collect);
}
/**
* 挂念集合 不带过滤
*
* @param listMapper 集合Mapper
* @return {@link DmListLambdaWrapper}
* @author Mc
* @date 2022/12/16 17:26
*/
public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper) {
S cs = listMapper.callWithServiceException(listVal);
return new DmListLambdaWrapper<>(cs);
}
/**
* 脱敏字符串字段
*
* @param dmType 脱敏类型
* @param mapper 脱敏字段
* @return {@link DmListLambdaWrapper}
* @author Mc
* @date 2022/12/16 17:26
*/
public <C extends String> DmListLambdaWrapper<Collection<F>, F> dmList2Str(DmTypeEnum dmType, DmParam<F, C> mapper) {
for (F f : listVal) {
C message = mapper.callWithServiceException(f);
if (null != message) {
String desensitized = DmUtils.desensitized(message, dmType, 1, 3, '*').toString();
FieldUtil.setField(f, mapper, desensitized);
// 反射获取字段名称
}
}
return new DmListLambdaWrapper<>(listVal);
}
/**
* 脱敏多个字符串字段,相同类型性能优先于多次使用 {@link DmListLambdaWrapper#dmList2Str(DmTypeEnum, DmParam)}
*
* @param dmType 脱敏类型
* @param mappers 脱敏字段
* @return {@link DmListLambdaWrapper}
* @author Mc
* @date 2022/12/16 17:27
*/
@SafeVarargs
public final <C extends String> DmListLambdaWrapper<Collection<F>, F> dmList2Str(DmTypeEnum dmType, final DmParam<F, C>... mappers) {
for (F f : listVal) {
for (DmParam<F, C> mapper : mappers) {
C message = mapper.callWithServiceException(f);
if (null != message) {
String desensitized = DmUtils.desensitized(message, dmType, 1, 3, '*').toString();
FieldUtil.setField(f, mapper, desensitized);
// 反射获取字段名称
}
}
}
return new DmListLambdaWrapper<>(listVal);
}
}
其他的方式可以自由拓展
用法:
DmLambdaWrapper.getInstance(user.getUserDetail())
.dmStr(DmTypeEnum.MOBILE_PHONE, UserDetail::getMobile)
.dmStr(DmTypeEnum.EMAIL, UserDetail::getEmail);
上面的例子将user
里的userDetail
字段中的mobile、email
进行了相对应的数据脱敏。