自定义序列化器实现内容脱敏功能

533 阅读4分钟

Spring Boot中,Spring MVC默认的JSON序列化器是Jackson。通过自定义实现一个序列化器接口,可以对特定注解的字段进行自定义序列化,从而实现以下功能场景:

  • 对身份证号码、手机号、姓名等敏感信息使用*隐藏一部分内容,或完全不显示,或显示成指定内容
  • 对LocalDateTime、LocalDate进行固定格式化
  • 对于空数组或空列表,前端希望是一个空列表[]而不是null
  • 金额在后端存的是以分为单位,传给前端时要以元为单位

自定义注解

通过@JsonSerialize注解可以指定一个字段序列化时使用的序列化器。可以为相对频繁使用的序列化器设定一个对应的自定义注解,或通过注解实现定制化功能。

一个数据脱敏自定义注解如下:

/**
 * 数据脱敏注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitivitySerializer.class)
public @interface SensitivityEncrypt {

    /**
     * 脱敏数据类型(必须指定类型)
     */
    SensitivityTypeEnum value();

    /**
     * 自定义时,前面有多少不需要脱敏的长度
     */
    int prefixNoMaskLen() default 1;

    /**
     * 自定义时,后面有多少不需要脱敏的长度
     */
    int suffixNoMaskLen() default 1;

    /**
     * 自定义时,用什么符号进行打码
     */
    String symbol() default "*";

    /**
     * 分隔符,填写后会先通过分隔符分隔后再按脱敏数据类型进行打码
     */
    String delimiter() default "";

}

/**
 * 脱敏类型枚举
 */
@Getter
enum SensitivityTypeEnum {

    /**
     * 真实姓名
     */
    REAL_NAME,

    /**
     * 身份证号
     */
    ID_CARD,

    /**
     * 邮箱
     */
    EMAIL,

    /**
     * 手机号
     */
    PHONE,

    /**
     * 根据原文长度自动隐藏
     */
    AUTO,

    /**
     * 自定义(此项需通过注解设置脱敏的前置后置长度)
     */
    CUSTOMER
}
/**
 * 入参属性解密反序列化器
 */
@AllArgsConstructor
@NoArgsConstructor
@Slf4j
public class DecryptionDeserializer extends JsonDeserializer<String> implements ContextualDeserializer {

    private DecryptedField decryptedField; // 注解

    private String dtoClassName; // 入参对象名,用于打印日志

    @Override
    public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
        String encryptedValue = jsonParser.getText();
        // 空白字符串不进行解密
        if (StringUtils.isBlank(encryptedValue) || decryptedField == null) {
            return encryptedValue;
        }
        try {
            return RsaUtil.decrypt(encryptedValue);
        } catch (CryptoException e) {
            log.error("{} 解密失败:", dtoClassName, e);
            throw new BaseException("参数错误");
        } catch (Exception e) {
            log.error("属性解密时发生错误:", e);
            throw new BaseException("系统错误");
        }
    }

    /**
    * 通过实现ContextualDeserializer接口的方法来判断是否使用该反序列化器
    */
    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty beanProperty) {
        if (beanProperty != null && Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
            DecryptedField annotation = beanProperty.getAnnotation(DecryptedField.class);
            String fullName = beanProperty.getMember().getFullName();

            if (annotation == null) {
                annotation = beanProperty.getContextAnnotation(DecryptedField.class);
            }
            if (annotation != null) {
                return new DecryptionDeserializer(annotation, fullName);
            }
        }
        return new DecryptionDeserializer();
    }
}
/**
* 响应对象
*/
@Data
public class UserVO implements Serializable {

    private static final long serialVersionUID = 1L;

	/**
	* 邮箱,会被自动按SensitivityUtil.hideEmail方法加密
	*/
    @SensitivityEncrypt(SensitivityTypeEnum.EMAIL)
    private String email;
    
}

序列化器

一个数据脱敏的序列化器如下:


/**
 * 敏感内容序列化类
 */
@NoArgsConstructor
@AllArgsConstructor
public class SensitivitySerializer extends JsonSerializer<String> implements ContextualSerializer {

    /**
     * 脱敏注解
     */
    private SensitivityEncrypt sensitivityEncrypt;

    /**
     * 序列化 数据处理
     */
    @Override
    public void serialize(final String origin, final JsonGenerator jsonGenerator,
                          final SerializerProvider serializerProvider) throws IOException {
        if (StringUtils.isNotBlank(origin) && null != sensitivityEncrypt.value()) {
            if (StringUtils.isNotBlank(sensitivityEncrypt.delimiter())) {
                jsonGenerator.writeString(Arrays.stream(origin.split(sensitivityEncrypt.delimiter()))
                        .map(o -> getEncryptString(o, sensitivityEncrypt))
                        .collect(Collectors.joining(sensitivityEncrypt.delimiter())));
            } else {
                jsonGenerator.writeString(getEncryptString(origin, sensitivityEncrypt));
            }
        } else {
            jsonGenerator.writeString("");
        }
    }

    private static String getEncryptString(String writeString, SensitivityEncrypt anno) {
        switch (anno.value()) {
            case CUSTOMER:
                return SensitivityUtil.desValue(writeString, anno.prefixNoMaskLen(), anno.suffixNoMaskLen(), anno.symbol());
            case REAL_NAME:
                return SensitivityUtil.hideChineseName(writeString);
            case ID_CARD:
                return SensitivityUtil.hideIdCard(writeString);
            case PHONE:
                return SensitivityUtil.hidePhone(writeString);
            case EMAIL:
                return SensitivityUtil.hideEmail(writeString);
            case AUTO:
            default:
                return SensitivityUtil.autoHideByLength(writeString);
        }
    }

    /**
     * 读取自定义注解SensitivityEncrypt 创建上下文所需
     */
    @Override
    public JsonSerializer<?> createContextual(final SerializerProvider serializerProvider,
                                              final BeanProperty beanProperty) throws JsonMappingException {
        if (beanProperty != null) {
            if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
                SensitivityEncrypt annotation = beanProperty.getAnnotation(SensitivityEncrypt.class);
                if (annotation == null) {
                    annotation = beanProperty.getContextAnnotation(SensitivityEncrypt.class);
                }
                if (annotation != null) {
                    return new SensitivitySerializer(annotation);
                }
                return new SensitivitySerializer();
            }
            return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
        }
        return serializerProvider.findNullValueSerializer(null);
    }
}

一个价格分自动转元的序列化类如下:

/**
 * 价格分转元序列化类
 */
@NoArgsConstructor
@AllArgsConstructor
public class PriceFenToYuanSerializer extends JsonSerializer<Integer> implements ContextualSerializer {

    private PriceFenToYuan priceFenToYuan;

    /**
     * 序列化 数据处理
     */
    @Override
    public void serialize(final Integer origin, final JsonGenerator jsonGenerator,
                          final SerializerProvider serializerProvider) throws IOException {
        if (origin != null) {
            if (priceFenToYuan != null) {
                BigDecimal fenBigDecimal = new BigDecimal(Integer.toString(origin));
                BigDecimal yuanBigDecimal = fenBigDecimal.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
                jsonGenerator.writeString(String.format("%.2f", yuanBigDecimal));
            } else {
                jsonGenerator.writeNumber(origin);
            }
        } else {
            jsonGenerator.writeNull();
        }
    }

    /**
     * 读取自定义注解PriceFenToYuan 创建上下文所需
     */
    @Override
    public JsonSerializer<?> createContextual(final SerializerProvider serializerProvider,
                                              final BeanProperty beanProperty) throws JsonMappingException {
        if (beanProperty != null) {
            if (Objects.equals(beanProperty.getType().getRawClass(), Integer.class)) {
                PriceFenToYuan sensitivityEncrypt = beanProperty.getAnnotation(PriceFenToYuan.class);
                if (sensitivityEncrypt == null) {
                    sensitivityEncrypt = beanProperty.getContextAnnotation(PriceFenToYuan.class);
                }
                return new PriceFenToYuanSerializer(sensitivityEncrypt);
            }
            return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
        }
        return serializerProvider.findNullValueSerializer(null);
    }
}

工具类

public class SensitivityUtil {

    /**
     * 中文姓名:只显示第一个汉字,其他隐藏为星号,例如:王**
     */
    public static String hideChineseName(String chineseName) {
        if (chineseName == null) {
            return null;
        }
        return desValue(chineseName, 1, 0, "*");
    }

    /**
     * 邮箱:隐藏格式为:l****3@qq.com
     */
    public static String hideEmail(String email) {
        String emailName = email.split("@")[0];
        if (emailName.length() <= 2) {
            return email.replaceAll("(.*?)(@\\w+\\.[a-z]+(\\.[a-z]+)?)", "****$2");
        }
        return email.replaceAll("(.?)(.+)(.)(@\\w+\\.[a-z]+(\\.[a-z]+)?)", "$1****$3$4");
    }

    /**
     * 手机号:隐藏中间5位:130*****029
     */
    public static String hidePhone(String phone) {
        return phone.replaceAll("(.{3}).{5}(.{3})", "$1*****$2");
    }

    /**
     * 身份证:隐藏格式为:110101********11**
     */
    public static String hideIdCard(String idCard) {
        return idCard.replaceAll("(.{6}).{8}(.{2}).{2}", "$1********$2**");
    }

    /**
     * 根据原文长度自动隐藏,原文太短则显示10个星号
     */
    public static String autoHideByLength(String sensitivityString) {
        if (sensitivityString.length() > 9) {
            return desValue(sensitivityString, 3, 3, "*");
        } else if (sensitivityString.length() > 3) {
            return desValue(sensitivityString, 0, 3, "*");
        }
        return "**********";
    }

    /**
     * 对字符串进行脱敏操作
     *
     * @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();
    }

}