springboot实现数据脱敏(重写序列化方法)
前言:对敏感数据进行隐藏的做法成为脱敏,实际项目中为了符合等保要求,需要对重要数据进行脱敏显示,例如非管理员不能查看其他用户的手机号登信息。在实现脱敏的方式中,最直观就是通过在接口中对敏感字符串进行替换,但此方式会增加代码量,耦合大,增加维护成本,因此采用自定义序列化方式。
springboot中在controller返回后,向前端发送数据前,需要经过序列化,将数据转化为前端能接收的数据类型,例如json或者xml,通过修改属性的序列化方法,将敏感字段做处理,实现数据脱敏操作。
以下内容基本搬运自文章【SpringBoot中利用自定义注解优雅地实现隐私数据脱敏(加密显示)】
来源链接: https://blog.csdn.net/qq_36737803/article/details/122366043
1. 创建枚举类
public enum DesensitizeTypeEnum {
/** 自定义(此项需设置脱敏的范围)*/
CUSTOMER,
/** 姓名(有默认脱敏规则,设置脱敏范围参数不生效) */
NAME,
/** 身份证号(有默认脱敏规则,设置脱敏范围参数不生效) */
ID_CARD,
/** 手机号(有默认脱敏规则,设置脱敏范围参数不生效) */
PHONE,
/** 邮箱(有默认脱敏规则,设置脱敏范围参数不生效) */
EMAIL;
}
这里枚举类的作用是定义默认脱敏行为,实现枚举类型的脱敏方式,因为像邮箱这种特殊格式,通过特定的正则表达式脱敏效果会更好。
2. 创建自定义注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside // 告诉jackson这是个自定义Jackson注解,在jackson序列化时被关注
@JsonSerialize(using = DesensitizeSerializer.class) //告诉jackson被改注解修饰的字段,应该使用自定义序列化器
public @interface DesensitizeField {
/**
* 脱敏数据类型(没给默认值,所以使用时必须指定type)
*/
DesensitizeTypeEnum type();
/**
* 前置不需要打码的长度
*/
int prefixNoMaskLen() default 1;
/**
* 后置不需要打码的长度
*/
int suffixNoMaskLen() default 1;
/**
* 用什么打码
*/
String symbol() default "*";
}
3. 创建自定义序列化器
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer {
// 脱敏类型
private DesensitizeTypeEnum type;
// 前几位不脱敏
private Integer prefixNoMaskLen;
// 最后几位不脱敏
private Integer suffixNoMaskLen;
// 用什么打码
private String symbol;
/**
* 标有脱敏注解的字段调用的序列化方法
* @param value 脱敏前字符串
* @param gen 用于输出生成的Json内容的生成器
* @param serializers 提供序列化对象的工厂,可提供当前序列化器和其他序列化器
* @throws IOException
*/
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 对序列化对象判空处理,只有非空对象才进行json输出
if (!ObjectUtils.isEmpty(value)) {
// 写入序列化结果,DesensitizeUtils.desensitize()为自定义字符串脱敏方法
gen.writeString(DesensitizeUtils.desensitize(value,this));
}
}
/**
* 构建自定义序列化器的上下文,判断序列化对象是否走自定义序列化器(通过判断是否带有自定义序列化注解)
* @param serializerProvider 提供序列化对象的工厂
* @param beanProperty 要序列化的对象
* @return
* @throws JsonMappingException
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
if (null != beanProperty) {
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
// 获取序列化对象上的自定义序列化注解
DesensitizeField desensitizeField = beanProperty.getAnnotation(DesensitizeField.class);
if (null == desensitizeField) {
desensitizeField = beanProperty.getContextAnnotation(DesensitizeField.class);
}
// 如果注解对象不为空,需要走自定义脱敏序列化器
if (null != desensitizeField) {
return new DesensitizeSerializer(desensitizeField.type(), desensitizeField.prefixNoMaskLen(),
desensitizeField.suffixNoMaskLen(), desensitizeField.symbol());
}
}
// 如果对象不为空或者没有自定义脱敏注解标注,走其他类型序列化器
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
// 如果对象为空,走空序列化器
return serializerProvider.findNullValueSerializer(null);
}
}
4.创建脱敏方法类
public class DesensitizeUtils {
/**
* 判断脱敏类型,根据不同脱敏类型选择不同脱敏方法进行脱敏
* @param value 原始字符串数据
* @param serializer 自定义的序列化其,主要是获取脱敏参数
* @return 脱敏后字符串
*/
public static String desensitize(String value, DesensitizeSerializer serializer){
String desensitizeValue;
switch (serializer.getType()){
case CUSTOMER:
desensitizeValue = desValue(value, serializer.getPrefixNoMaskLen(),
serializer.getSuffixNoMaskLen(), serializer.getSymbol());
break;
case NAME:
desensitizeValue = hideChineseName(value);
break;
case EMAIL:
desensitizeValue = hideEmail(value);
break;
case PHONE:
desensitizeValue = hidePhone(value);
break;
case ID_CARD:
desensitizeValue = hideIDCard(value);
break;
default:
throw new IllegalArgumentException("unknown privacy type enum " + serializer.getType());
}
return desensitizeValue;
}
/**
* 中文名脱敏
*/
public static String hideChineseName(String chineseName) {
if (ObjectUtils.isEmpty(chineseName)) {
return null;
}
if (chineseName.length() <= 2) {
return desValue(chineseName, 1, 0, "*");
}
return desValue(chineseName, 1, 1, "*");
}
/**
* 手机号脱敏
*/
public static String hidePhone(String phone) {
if (ObjectUtils.isEmpty(phone)) {
return null;
}
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); // 隐藏中间4位
// return desValue(phone, 3, 4, "*"); // 隐藏中间4位
// return desValue(phone, 7, 0, "*"); // 隐藏末尾4位
}
/**
* 邮箱脱敏
*/
public static String hideEmail(String email) {
if (ObjectUtils.isEmpty(email)) {
return null;
}
return email.replaceAll("(\\w?)(\\w+)(\\w)(@\\w+\\.[a-z]+(\\.[a-z]+)?)", "$1****$3$4");
}
/**
* 身份证号脱敏
*/
public static String hideIDCard(String idCard) {
if (ObjectUtils.isEmpty(idCard)) {
return null;
}
return idCard.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1*****$2");
}
/**
* 对字符串进行脱敏操作
* @param origin 原始字符串
* @param prefixNoMaskLen 左侧需要保留几位明文字段
* @param suffixNoMaskLen 右侧需要保留几位明文字段
* @param maskStr 用于遮罩的字符串, 如'*'
* @return 脱敏后结果
*/
public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {
if (ObjectUtils.isEmpty(origin)) {
return origin;
}
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();
}
}
至此,自定义序列化方法实现脱敏构建完成
5. 测试
构建实体类,使用自定义注解
@Data
@ToString
public class UserInfo {
private String userId;
@DesensitizeField(type = DesensitizeTypeEnum.PHONE)
private String mobile;
@DesensitizeField(type = DesensitizeTypeEnum.CUSTOMER,prefixNoMaskLen = 0,suffixNoMaskLen = 0)
private String password;
@DesensitizeField(type = DesensitizeTypeEnum.ID_CARD)
private String identityId;
@DesensitizeField(type = DesensitizeTypeEnum.EMAIL)
private String email;
private String createTime;
}
测试接口
@GetMapping("test")
public UserInfo getUserInfo(){
UserInfo userInfo = new UserInfo();
userInfo.setUserId("100001");
userInfo.setMobile("12345678910");
userInfo.setPassword("Mm123456");
userInfo.setIdentityId("430444199901017878");
userInfo.setEmail("zhangsanlisi123@qq.com");
userInfo.setCreateTime(null);
return userInfo;
}
测试结果如下图