有道无术,术尚可求,有术无道,止于术。
本系列Jackson 版本 2.17.0
本系列Spring Boot 版本 3.2.4
源码地址:https://gitee.com/pearl-organization/study-jaskson-demo
文章目录
- 1. 概述
- 2. 实现思路
- 3. 案例演示
- 3.1 脱敏规则
- 3.2 自定义注解
- 3.3 自定义序列化器
- 3.4 测试
1. 概述
数据脱敏指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。在开发、测试和其它非生产环境以及外包环境中安全地使用脱敏后的真实数据集。例如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。
按照脱敏方式分为:
- 静态数据脱敏:按照脱敏规则一次性完成大批量数据的变形转换处理
- 动态数据脱敏:按照脱敏规则对于外部申请访问的数据进行即时处理并返回脱敏后的结果
随着 《网络安全法》 的颁布施行,对个人隐私数据的保护已经上升到法律层面。传统的应用系统普遍缺少对个人隐私数据的保护措施。数据脱敏,可实现在不需要对生产数据库中的数据进行任何改变的情况下,依据用户定义的脱敏规则,对生产数据库返回的数据进行专门的加密、遮盖和替换,确保生产环境的敏感数据能够得到保护。
2. 实现思路
一般可以在以下两种时机进行脱敏操作:
- 数据库查询返回对象时
- 序列化返回浏览器时
Spring Web
默认使用Jackson
作为HTTP
消息转换器的Json
处理框架,那么可以自定义Jackson
的序列化器对字段进行脱敏处理。
3. 案例演示
演示需求:
- 身份证号显示为:
43************6363
- 手机号显示为:
135****8888
3.1 脱敏规则
常用的脱敏规则有:
MD5
:直接使用MD5
计算- 遮盖脱敏:使用特殊字符遮盖,比如
*
- 替换脱敏:使用码表进行替换
演示需求中需要使用遮盖脱敏规则,将部分信息使用*
进行遮盖。
3.2 自定义注解
参考Apache ShardingSphere的遮盖自 X
至 Y
脱敏算法,定义一个脱敏注解,指定开始、结束位置,之间的字符都会使用指定的字符进行遮盖。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.ANNOTATION_TYPE})
@JacksonAnnotationsInside
@JsonSerialize(using = MaskFromXToYJsonSerializer.class)
public @interface MaskFromXToYMask {
/**
* 起始位置 (从 0 开始计数)
*/
int from();
/**
* 结束位置(从 0 开始计数)
*/
int to();
/**
* 替换字符,默认*
*/
String replaceChar() default "*";
}
定义身份证脱敏注解,遮盖2-13
位置的字符:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@MaskFromXToYMask(from =2 ,to =13)
public @interface MaskIdNum {
}
定义手机号脱敏注解,遮盖3-6
位置的字符:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@MaskFromXToYMask(from =3 ,to =6)
public @interface MaskPhone {
}
3.3 自定义序列化器
自定义序列化器MaskFromXToYJsonSerializer
,获取字段上的脱敏注解,序列化时进行遮盖写出:
public class MaskFromXToYJsonSerializer extends StdSerializer<String> implements ContextualSerializer {
private int from;
private int to;
private String replaceChar;
public MaskFromXToYJsonSerializer(Class<String> t, int from, int to, String replaceChar) {
super(t);
this.from = from;
this.to = to;
this.replaceChar = replaceChar;
}
public MaskFromXToYJsonSerializer() {
super(String.class);
}
/**
* 序列化
*/
@Override
public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
// 1. 校验
if (this.from >= this.to) {
throw new RuntimeException("起始位置不能大于等于结束位置");
}
if (StrUtil.isEmpty(this.replaceChar)) {
throw new RuntimeException("替换字符不能为空");
}
// 2. 序列化
if (StrUtil.isEmpty(str)) {
// 为空
jsonGenerator.writeString("");
} else if (str.length() <= this.from) {
// 字符长度小于开始位置
jsonGenerator.writeString(str);
} else {
// 脱敏写出
char[] chars = str.toCharArray();
int i = this.from;
for (int minLength = Math.min(this.to, chars.length - 1); i <= minLength; ++i) {
chars[i] = this.replaceChar.charAt(0);
}
jsonGenerator.writeString(new String(chars));
}
}
/**
* 根据字段注解,返回序列化器,仅在在第一次序列化时调用
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializers, BeanProperty property) throws JsonMappingException {
// 1. 属性为 Null
if (property == null) {
return serializers.findNullValueSerializer(null);
}
// 2. 属性的类型
Class<?> rawClass = property.getType().getRawClass();
if (CharSequence.class.isAssignableFrom(rawClass)) {
// 3. 字符串类型,返回脱敏序列化器
MaskFromXToYMask annotation = property.getAnnotation(MaskFromXToYMask.class);
if (annotation == null) {
annotation = property.getContextAnnotation(MaskFromXToYMask.class);
}
if (annotation != null) {
return new MaskFromXToYJsonSerializer(String.class, annotation.from(), annotation.to(), annotation.replaceChar());
}
}
return serializers.findValueSerializer(property.getType(), property);
}
}
3.4 测试
定义一个用户对象,添加脱敏注解:
@Data
@ToString
public class UserVO implements Serializable {
Long id;
String username;
List<String> roleList;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
Date birthday;
@MaskIdNum
String idNum;
@MaskPhone
String phone;
}
常见一个访问接口:
@RequestMapping("/test")
public UserVO test() {
UserVO userVO = new UserVO();
userVO.setId(1699657986705854464L);
userVO.setUsername("jack");
userVO.setBirthday(new Date());
userVO.setIdNum("430852195602056363");
userVO.setPhone("13536238888");
List<String> roleList = new ArrayList<>();
roleList.add("管理员");
roleList.add("经理");
userVO.setRoleList(roleList);
return userVO;
}
访问接口: