前言

数据脱敏是一种数据保护技术,它可以对敏感数据进行处理,使其失去原始意义,从而保护数据隐私。通俗地说,就是将原始数据中敏感信息的部分替换或删除,例如:手机号码中的一部分数字替换为星号,或是姓名中的一些字母用其他字母代替等等。数据脱敏可以保护个人隐私,防止个人信息被泄露、滥用,同时也符合现代法律法规对个人信息保护的要求,是网络安全和信息保护中不可或缺的一环。

本文将通过使用注解完成对字段的自定义脱敏功能。

正文

本文将介绍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进行了相对应的数据脱敏。

如果觉得我的文章对你有用,请随意赞赏