自定义Jackson数据脱敏
Jackson是Spring默认的序列化框架,以下将通过自定义Jackson注解,实现在序列化过程中对属性值进行处理。
定义一个注解,标注在需要脱敏的字段上
代码
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
27package com.example.demo.example.annotation;
import com.example.demo.example.SecretJsonConfig.SecretJsonSerializer;
import com.example.demo.example.SecretJsonConfig.SecretStrategy;
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;
/**标注在字段上*/
/**一般用于将其他的注解一起打包成"组合"注解*/
/**对标注注解的字段采用哪种序列化器进行序列化*/
public SecretColumn {
/**脱敏策略*/
SecretStrategy strategy();
}
定义字段序列化策略,因为不同类型数据有不同脱敏后的展现形式。以下通过枚举类方式实现几种策略:
代码
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
40package com.example.demo.example.SecretJsonConfig;
import lombok.Getter;
import java.util.function.Function;
/**
* 脱敏策略,不同数据可选择不同的策略
*/
public enum SecretStrategy {
/**
* 用户名脱敏
*/
USERNAME(str -> str.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
/**
* 身份证脱敏
*/
ID_CARD(str -> str.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1****$2")),
/**
* 手机号脱敏
*/
PHONE(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;
SecretStrategy(Function<String, String> desensitizer) {
this.desensitizer = desensitizer;
}
}
定义一个Jackson序列化器,可以对标注了@SecretColumn 的注解进行处理
代码
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
70package com.example.demo.example.SecretJsonConfig;
import com.example.demo.example.annotation.SecretColumn;
import com.example.demo.example.mapper.ConfigMapper;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Objects;
/**
* 序列化器实现
*/
//@Component//不加@Component注解,ConfigMapper无法注入
public class SecretJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SecretStrategy secretStrategy;
//@Autowired
//ConfigMapper configMapper;
private String secretSwitch;
/**
* 步骤一
* 方法来源于ContextualSerializer,获取属性上的注解属性,同时返回一个合适的序列化器
*/
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
// 获取自定义注解
SecretColumn annotation = beanProperty.getAnnotation(SecretColumn.class);
//查询数据库脱敏开关状态(更改数据库之后会报null异常,暂时放弃此条件判断)
//secretSwitch = configMapper.getSecretFlag();
//脱敏开关打开时,才进行脱敏处理
if(!Objects.isNull(secretSwitch) && "on".equals(secretSwitch)){
// 注解不为空,且标注的字段为String
if(Objects.nonNull(annotation) && Objects.equals(String.class, beanProperty.getType().getRawClass())){
this.secretStrategy = annotation.strategy();
// 符合我们自定义情况,返回本序列化器,将顺利进入到该类中的serialize()方法中
return this;
}
}
// 注解为空,字段不为String,寻找合适的序列化器进行处理
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
/**
* 步骤二
* 方法来源于JsonSerializer<String>:指定返回类型为String类型,serialize()将修改后的数据返回
*/
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if(Objects.isNull(secretStrategy)){
// 定义策略为空,返回原字符串
jsonGenerator.writeString(s);
}else {
// 定义策略不为空,返回策略处理过的字符串
jsonGenerator.writeString(secretStrategy.getDesensitizer().apply(s));
}
}
}
实体类中使用@SecretColumn注解
代码
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
39package com.example.demo.example.entity;
import com.example.demo.example.SecretJsonConfig.SecretStrategy;
import com.example.demo.example.annotation.SecretColumn;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
public class UserEntity {
/**
* 真实姓名
*/
private String realName;
/**
* 地址
*/
private String address;
/**
* 电话号码
*/
private String phoneNumber;
/**
* 身份证号码
*/
private String idCard;
}
测试
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package com.example.demo.example;
import com.example.demo.example.entity.UserEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
public class TestController {
public UserEntity test(){
UserEntity user = new UserEntity();
user.setRealName("张三(学生)")
.setPhoneNumber("18000000000")
.setAddress("蓝星地球村太平洋100号")
.setIdCard("610000000000000000");
// System.out.println(user);
return user;
}
}测试结果
结果
1
2
3
4
5
6
7
8// http://localhost:8080/secret/test
{
"realName": "张*(学生)",
"address": "蓝星地****太平洋10****",
"phoneNumber": "180****0000",
"idCard": "6100****0000"
}
POM依赖
代码
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
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR10</spring-cloud.version>
</properties>
<dependencies>
<!--spring-boot-starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--spring-boot-starter-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!--mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!--mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--dynamic多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>