利用Hutool+自定义注解实现数据脱敏

利用Hutool+自定义注解实现数据脱敏

前言

我们在使用手机银行的时候经常能看到APP上会将银行卡的卡号中间部分给隐藏掉使用 * 来代替,在某些网站上查看一些业务密码时(例如签到密码等)也会使用 * 来隐藏掉真正的密码,那么这种方式是如何实现的呢?

Hutool

Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。

Hutool中的工具方法来自每个用户的精雕细琢,它涵盖了Java开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当;

Hutool是项目中 util 包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的bug。

我们这篇文章的实现思路就基于Hutool来实现,在Hutool中提供了一个名为 DesensitizedUtil 的工具类,我们使用这个工具类来加密。

首先我们先来看一下这个类里的具体实现,如下:

image.png

我们可以看到映入眼帘的除了一个无参构造之外就是一个名为 desensitized 的方法,这个方法就是我们加密的主要方法,里面利用了 switch…case 方法来区分不同的加密方法。我们可以来写一个单元测试来测试一下通过这个方法加密后是什么样的。

image.png

以上为加密后的信息,里面我使用了不同的类型来进行加密,目前最新版的Hutool支持脱敏加密的类型如下:

  1. 用户ID
  2. 中文名
  3. 密码
  4. 地址
  5. 邮箱
  6. 座机号
  7. 手机号
  8. 中国大陆的车牌号
  9. 银行卡号
  10. IPv4地址
  11. IPv6地址
  12. 自定义脱敏

实现

通过以上的示例我们就可以开始编写我们自己的脱敏操作了,首先我们要先根据以上Hutool中提供的脱敏类型来编写我们自己的类型(如嫌麻烦也可省略此步骤,直接使用DesensitizedUtil中的DesensitizedType)

编写数据脱敏类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* @author Bummon
* @description 数据脱敏策略
* @date 2023-09-01 17:43
*/
public enum DataMaskingType {

/**
* 用户ID
*/
USER_ID,
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份证号
*/
ID_CARD,
/**
* 座机
*/
FIXED_PHONE,
/**
* 手机号
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 邮箱
*/
EMAIL,
/**
* 密码
*/
PASSWORD,
/**
* 中国大陆车牌号
*/
CAR_LICENSE,
/**
* 银行卡号
*/
BANK_CARD,
/**
* IPv4地址
*/
IPV4,
/**
* IPv6地址
*/
IPV6,
/**
* 自定义类型
*/
CUSTOM;

}

编写自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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;

/**
* @author Bummon
* @description 数据脱敏自定义注解
* @date 2023-09-01 18:01
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DataMaskingSerialize.class)
public @interface DataMasking {

/**
* 数据脱敏类型
*/
DataMaskingType type() default DataMaskingType.CUSTOM;

/**
* 脱敏开始位置(包含)
*/
int start() default 0;

/**
* 脱敏结束位置(不包含)
*/
int end() default 0;

}

需要注意的是:当DataMaskingType为 CUSTOM 时,才需要填写 startend ,且这两个参数才会生效,且 start 中是包含当前下标的字符的,而 end 不包含当前下标的字符。

编写自定义序列化类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.DesensitizedUtil;
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 lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.util.Objects;

/**
* @author Bummon
* @description
* @date 2023-09-01 18:14
*/
@AllArgsConstructor
@NoArgsConstructor
public class DataMaskingSerialize extends JsonSerializer implements ContextualSerializer {

private DataMaskingType type;

private Integer start;

private Integer end;

@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
String value = (String) o;
switch (type) {
//userId
case USER_ID:
jsonGenerator.writeString(String.valueOf(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.USER_ID)));
break;
//中文名
case CHINESE_NAME:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.CHINESE_NAME));
break;
//身份证号
case ID_CARD:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.ID_CARD));
break;
//座机
case FIXED_PHONE:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.FIXED_PHONE));
break;
//手机号
case MOBILE_PHONE:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.MOBILE_PHONE));
break;
//地址
case ADDRESS:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.ADDRESS));
break;
//邮箱
case EMAIL:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.EMAIL));
break;
case BANK_CARD:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.BANK_CARD));
break;
//密码
case PASSWORD:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.PASSWORD));
break;
//中国大陆车牌号
case CAR_LICENSE:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.CAR_LICENSE));
break;
case IPV4:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.IPV4));
break;
case IPV6:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.IPV6));
break;
//自定义
case CUSTOM:
jsonGenerator.writeString(CharSequenceUtil.hide(value, start, end));
break;
default:
break;
}
}

@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
if (Objects.nonNull(beanProperty)) {
//判断是否为string类型
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
DataMasking anno = beanProperty.getAnnotation(DataMasking.class);
if (Objects.isNull(anno)) {
anno = beanProperty.getContextAnnotation(DataMasking.class);
}
if (Objects.nonNull(anno)) {
return new DataMaskingSerialize(anno.type(), anno.start(), anno.end());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(null);
}
}

我们继承于 JsonSerializer 并实现了 ContextualSerializer 中的方法,并对我们自定义注解声明的字段进行拦截和脱敏加密操作,接下来我们可以来测试一下效果。

测试

因为是实例化的时候才会被脱敏,那我们就创建一个实体类来存放我们需要加密的信息。

编写测试实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import com.bummon.mask.DataMasking;
import com.bummon.mask.DataMaskingType;
import lombok.*;

/**
* @author Bummon
* @description
* @date 2023-09-01 18:29
*/
@Data
@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class TestEntity {

@DataMasking(type = DataMaskingType.USER_ID)
private Integer userId;

@DataMasking(type = DataMaskingType.CHINESE_NAME)
private String userName;

@DataMasking(type = DataMaskingType.ADDRESS)
private String address;

@DataMasking(type = DataMaskingType.ID_CARD)
private String idCard;

@DataMasking(type = DataMaskingType.FIXED_PHONE)
private String fixedPhone;

@DataMasking(type = DataMaskingType.MOBILE_PHONE)
private String mobilePhone;

@DataMasking(type = DataMaskingType.EMAIL)
private String email;

@DataMasking(type = DataMaskingType.PASSWORD)
private String password;

@DataMasking(type = DataMaskingType.CAR_LICENSE)
private String carLicense;

@DataMasking(type = DataMaskingType.BANK_CARD)
private String bankCard;

@DataMasking(type = DataMaskingType.IPV4)
private String ipv4;

@DataMasking(type = DataMaskingType.IPV6)
private String ipv6;

@DataMasking(type = DataMaskingType.CUSTOM,start = 3,end = 9)
private String custom;

/**
* 不进行数据脱敏的字段
*/
private String noMask;


}

编写测试Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import com.bummon.entity.TestEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author Bummon
* @description
* @date 2023-09-01 18:39
*/
@RestController
public class TestController {

@GetMapping("/test")
public TestEntity test() {
return TestEntity.builder()
.userId(1234567890)
.userName("张三")
.password("12")
.address("河南省郑州市中原区")
.email("xxxx@xx.com")
.fixedPhone("0838-5553792")
.mobilePhone("13888888888")
.carLicense("豫P3U253")
.bankCard("1679374639283740")
.idCard("412711223344556677")
.ipv4("192.168.1.236")
.ipv6("abcd:1234:aCA9:123:4567:089:0:0000")
.custom("289073458794")
.noMask("我是不需要数据脱敏的字段")
.build();
}

}

接下来我们启动项目来看测试一下得到的是否为我们预期的数据:

image.png

我们可以看到,我们加了注解的字段都被正确的脱敏了,而没加注解的字段会正常显示。

总结

我们使用了HutoolDesensitizedUtil中的 desensitized 方法来实现数据脱敏,在 CUSTOM 类型的脱敏字段中,startend 两个属性是必填的,且 start 包含当前下标,而 end 不包含当前下标。

本文结束,感谢观看。