前言
我们在使用手机银行的时候经常能看到APP上会将银行卡的卡号中间部分给隐藏掉使用 * 来代替,在某些网站上查看一些业务密码时(例如签到密码等)也会使用 * 来隐藏掉真正的密码,那么这种方式是如何实现的呢?
Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。
Hutool中的工具方法来自每个用户的精雕细琢,它涵盖了Java开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当;
Hutool是项目中 util 包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的bug。
我们这篇文章的实现思路就基于Hutool来实现,在Hutool中提供了一个名为 DesensitizedUtil
的工具类,我们使用这个工具类来加密。
首先我们先来看一下这个类里的具体实现,如下:
![image.png](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
我们可以看到映入眼帘的除了一个无参构造之外就是一个名为 desensitized
的方法,这个方法就是我们加密的主要方法,里面利用了 switch…case 方法来区分不同的加密方法。我们可以来写一个单元测试来测试一下通过这个方法加密后是什么样的。
![image.png](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
以上为加密后的信息,里面我使用了不同的类型来进行加密,目前最新版的Hutool支持脱敏加密的类型如下:
- 用户ID
- 中文名
- 密码
- 地址
- 邮箱
- 座机号
- 手机号
- 中国大陆的车牌号
- 银行卡号
- IPv4地址
- IPv6地址
- 自定义脱敏
实现
通过以上的示例我们就可以开始编写我们自己的脱敏操作了,首先我们要先根据以上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
|
public enum DataMaskingType {
USER_ID,
CHINESE_NAME,
ID_CARD,
FIXED_PHONE,
MOBILE_PHONE,
ADDRESS,
EMAIL,
PASSWORD,
CAR_LICENSE,
BANK_CARD,
IPV4,
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;
@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
时,才需要填写 start
和 end
,且这两个参数才会生效,且 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;
@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) { 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)) { 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.*;
@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;
@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](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
我们可以看到,我们加了注解的字段都被正确的脱敏了,而没加注解的字段会正常显示。
总结
我们使用了Hutool的DesensitizedUtil中的 desensitized
方法来实现数据脱敏,在 CUSTOM 类型的脱敏字段中,start
和 end
两个属性是必填的,且 start
包含当前下标,而 end
不包含当前下标。
本文结束,感谢观看。