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();
}
}