目录
🍸前言
🍻一、Jackson 序列化库
🍺二、方案实践
2.1 环境准备
2.2 依赖引入
2.3 代码编写
💞️三、接口测试
🍹四、章末
🍸前言
小伙伴们大家好,最近也是很忙啊,上次的文章分享了 Caffeine 本地缓存,文章链接如下:
【Caffeine】⭐️SpringBoot 项目整合 Caffeine 实现本地缓存_springboot caffeine-CSDN博客
最近在别的技术分享上看到了数据脱敏的介绍,感觉平时接触的挺多的,比如个人私密信息在页面上显示的时候需要对部分信息加密,就像手机号中间几个位置用 “*” 代替(如下图),具体的实现也有很多方式,比如单独某一个接口需要加密,那么可以在接口返回值时处理下数据即可,但是如果过多的接口有此需求,挨个处理显然费时费力并且增加代码冗余,这个时候就可以选用全局处理的解决方案,实现一次编写,到处实现,具体的方案就是可以在序列化时处理数据 ,序列化工具常用的 Jackson 日常接触也很多
🍻一、Jackson 序列化库
Jackson 是一个流行的 Java 序列化/反序列化库,专门用于处理 JSON 数据。它提供了一组功能强大且灵活的 API,可以帮助开发者在 Java 对象和 JSON 数据之间进行转换。有以下几个特性,灵活、高效、应用广泛(整合各种框架),比如我们经常接触的 web 系统,就是将后端的数据 Json 序列化后传递给前端实现的
🍺二、方案实践
2.1 环境准备
测试项目是基于 SpringBoot 框架的项目实现,另外接口测试使用 Apipost 实现模拟请求
2.2 依赖引入
因为使用的 SpringBoot 框架,所以在创建项目的时候就已经勾选了 web 模块,而这些模块已经集成了相关的 Jackson 包,依赖如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
如果项目中没有的话,也可以手动引入,依赖如下:
注:手动引入的时候,需要注意下版本问题,项目启动的时候可能会遇到如下情况,这种情况我在博客上搜索了相关的文章,有讲解的不是很多,但是可行的一种方案就是自己不要指定版本,使用父依赖的即可解决
Error starting Tomcat context. Exception: org.springframework.beans.factory.BeanCreationException. Message: Error creating bean with name 'formContentFilter' defined in class path resource
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<!-- <version>2.9.8</version>-->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<!-- <version>2.9.8</version>-->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<!-- <version>2.9.8</version>-->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<!-- <version>2.13.0</version> <!– 替换为与你的Spring Boot兼容的版本 –>-->
</dependency>
2.3 代码编写
2.3.1 脱敏数据类型枚举
指定哪些类型的数据需要脱敏,命名为枚举类,方便后续调用
/**
* @author HuangBen
*/
public enum SensitiveEnum {
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份证号
*/
ID_CARD,
/**
* 手机号
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 电子邮件
*/
EMAIL,
/**
* 银行卡
*/
BANK_CARD,
}
2.3.2 处理隐私数据工具类
这里将已有的处理工具封装为一个工具类,方便设置参数以及调用,这里导入的工具类如果报错,说明并没有引入依赖,依赖如下:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
import org.apache.commons.lang3.StringUtils;
/**
* @author HuangBen
*/
public class SensitiveInfoUtils {
/**
* [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**>
*/
public static String chineseName(final String fullName) {
if (StringUtils.isBlank(fullName)) {
return "";
}
final String name = StringUtils.left(fullName, 1);
return StringUtils.rightPad(name, StringUtils.length(fullName), "*");
}
/**
* [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**>
*/
public static String chineseName(final String familyName, final String givenName) {
if (StringUtils.isBlank(familyName) || StringUtils.isBlank(givenName)) {
return "";
}
return chineseName(familyName + givenName);
}
/**
* [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。<例子:420**********5762>
*/
public static String idCardNum(final String id) {
if (StringUtils.isBlank(id)) {
return "";
}
return StringUtils.left(id, 3).concat(StringUtils
.removeStart(StringUtils.leftPad(StringUtils.right(id, 4), StringUtils.length(id), "*"),
"***"));
}
/**
* [手机号码] 前三位,后四位,其他隐藏<例子:138******1234>
*/
public static String mobilePhone(final String num) {
if (StringUtils.isBlank(num)) {
return "";
}
return StringUtils.left(num, 3).concat(StringUtils
.removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"),
"***"));
}
/**
* [地址] 只显示到地区,不显示详细地址;我们要对个人信息增强保护<例子:北京市海淀区****>
*
* @param sensitiveSize 敏感信息长度
*/
public static String address(final String address, final int sensitiveSize) {
if (StringUtils.isBlank(address)) {
return "";
}
final int length = StringUtils.length(address);
return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");
}
/**
* [电子邮箱] 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示<例子:g**@163.com>
*/
public static String email(final String email) {
if (StringUtils.isBlank(email)) {
return "";
}
final int index = StringUtils.indexOf(email, "@");
if (index <= 1) {
return email;
} else {
return StringUtils.rightPad(StringUtils.left(email, 1), index, "*")
.concat(StringUtils.mid(email, index, StringUtils.length(email)));
}
}
/**
* [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号<例子:6222600**********1234>
*/
public static String bankCard(final String cardNum) {
if (StringUtils.isBlank(cardNum)) {
return "";
}
return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart(
StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"),
"******"));
}
}
2.3.3 编写脱敏序列化实现类
这里就是先用刚才工具类处理数据再进行序列化
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;
/**
* @author HuangBen
*/
public class SensitiveSerialize extends JsonSerializer<String> implements ContextualSerializer {
/**
* 脱敏类型
*/
private SensitiveEnum type;
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
switch (this.type) {
case CHINESE_NAME: {
jsonGenerator.writeString(SensitiveInfoUtils.chineseName(s));
break;
}
case ID_CARD: {
jsonGenerator.writeString(SensitiveInfoUtils.idCardNum(s));
break;
}
case MOBILE_PHONE: {
jsonGenerator.writeString(SensitiveInfoUtils.mobilePhone(s));
break;
}
case ADDRESS: {
jsonGenerator.writeString(SensitiveInfoUtils.address(s, 4));
break;
}
case EMAIL: {
jsonGenerator.writeString(SensitiveInfoUtils.email(s));
break;
}
case BANK_CARD: {
jsonGenerator.writeString(SensitiveInfoUtils.bankCard(s));
break;
}
}
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
// 为空直接跳过
if (beanProperty != null) {
// 非 String 类直接跳过
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
SensitiveWrapped sensitiveWrapped = beanProperty.getAnnotation(SensitiveWrapped.class);
if (sensitiveWrapped == null) {
sensitiveWrapped = beanProperty.getContextAnnotation(SensitiveWrapped.class);
}
if (sensitiveWrapped != null) {
// 如果能得到注解,就将注解的 value 传入 SensitiveSerialize
return new SensitiveSerialize(sensitiveWrapped.value());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(beanProperty);
}
public SensitiveSerialize() {}
public SensitiveSerialize(final SensitiveEnum type) {
this.type = type;
}
}
2.3.4 编写脱敏注解类
通过此注解可以灵活标注哪些数据需要脱敏处理,并且标注对应的枚举类型;注意这里指定了序列化的实现类为我们自己刚定义的,具体实现的功能就是遇到此注解标注的字段,序列化的时候使用我们指定的序列化器,我们自定义的序列化器中对每种类型的字段都有相应的处理逻辑
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* @author HuangBen
*/
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerialize.class)
public @interface SensitiveWrapped {
/**
* 脱敏类型
* @return
*/
SensitiveEnum value();
}
2.3.5 编写测试实体类
这里指定了 mobile , idcard 两个字段需要脱敏,并且指明了按照哪一种数据类型脱敏处理
import lombok.Data;
/**
* @author HuangBen
*/
@Data
public class UserEntity {
/**
* 用户ID
*/
private Long userId;
/**
* 用户姓名
*/
private String name;
/**
* 手机号
*/
@SensitiveWrapped(SensitiveEnum.MOBILE_PHONE)
private String mobile;
/**
* 身份证号码
*/
@SensitiveWrapped(SensitiveEnum.ID_CARD)
private String idCard;
/**
* 年龄
*/
private String sex;
/**
* 性别
*/
private int age;
}
💞️三、接口测试
简单用一个 restful 接口测试下处理后的数据是否脱敏,接口如下:
利用接口请求工具,确实按照我们指定的字段以及类型进行脱敏处理了,测试成功,结果如下:
🍹四、章末
通过这种方式实现的数据脱敏处理,后续在使用的时候也很方便,有哪些需要处理的字段只需要在实体类的字段上方添加注解并且指定脱敏类型即可,符合开闭原则,并且遵循很少改动原有代码的规则;
文章到这里就结束了~