在实际项目中通常需要对用户的敏感信息比如身份号、家庭地址、手机号等进行脱敏操作以保证信息安全。
解决方案
-
每个接口单独处理,这种方式操作简单,但是重复工作量非常大,而且后续脱敏规则变更是需要逐个修改。
-
使用定义Jackson注解,在序列化的时候对敏感字段值进行处理,这种方式高效又优雅,省时省力,支持扩展。
这里我们使用注解的方式实现,在序列化的时候对敏感字段值进行处理。
项目结构如下
1. 新建脱敏注解类
在接口返回实体字段添加该注解并指定脱敏规则,接口返回数据当前字段会按规则脱敏。
package com.mfc.test.datamasking;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DataMaskingJsonSerializer.class)
public @interface DataMasking {
// 脱敏规则
MaskingRule maskingRule();
}
2.新建脱敏规则设置类
该类是个枚举值,设置字段的脱敏方式
package com.mfc.test.datamasking;
import lombok.Getter;
import java.util.function.Function;
/**
* 脱敏策略,不同数据可选择不同的策略
*/
@Getter
public enum MaskingRule {
/**
* 用户名脱敏
*/
USERNAME(str -> str.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
/**
* 身份证脱敏
*/
ID_CARD(str -> str.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1****$2")),
/**
* 手机号脱敏
*/
MOBILEPHONE(str -> str.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
/**
* 地址脱敏
*/
ADDRESS(str -> str.replaceAll("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****"));
private final Function<String, String> desensitizer;
MaskingRule(Function<String, String> desensitizer) {
this.desensitizer = desensitizer;
}
}
3.新建脱敏序列化器
package com.mfc.test.datamasking;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import java.io.IOException;
import java.util.Objects;
/**
* 序列化器
*/
public class DataMaskingJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private MaskingRule maskingRule;
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
// 获取自定义注解
DataMasking annotation = beanProperty.getAnnotation(DataMasking.class);
// 注解不为空,且标注的字段为String
if (Objects.nonNull(annotation) && Objects.equals(String.class, beanProperty.getType().getRawClass())) {
this.maskingRule = annotation.maskingRule();
return this;
}
// 注解为空,字段不为String,寻找合适的序列化器进行处理
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (Objects.isNull(maskingRule)) {
// 定义规则为空,返回原字符串
jsonGenerator.writeString(s);
} else {
// 定义规则不为空,返回处理后字符串
jsonGenerator.writeString(maskingRule.getDesensitizer().apply(s));
}
}
}
4.新建实体并为要脱敏的字段添加注解
package com.mfc.test.domain;
import com.mfc.test.datamasking.DataMasking;
import com.mfc.test.datamasking.MaskingRule;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
@ToString
@Accessors(chain = true)
@Data
public class Member {
private String username;
private String gender;
@DataMasking(maskingRule = MaskingRule.USERNAME)
private String realName;
@DataMasking(maskingRule = MaskingRule.ID_CARD)
private String idCard;
@DataMasking(maskingRule = MaskingRule.MOBILEPHONE)
private String mobilePhone;
@DataMasking(maskingRule = MaskingRule.ADDRESS)
private String address;
}
5.新建controller
package com.mfc.test.controller;
import com.mfc.test.domain.Member;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/getMember")
public Member test() {
Member member = new Member();
member.setUsername("zxh")
.setGender("男")
.setRealName("张小华")
.setIdCard("610122199003142113")
.setMobilePhone("15111111111")
.setAddress("四川省成都市武侯区武侯大街1033号");
return member;
}
}
访问http://localhost:8081/test/getMember
测试结果如下