Java使用Hutool+自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏
前言
我們在使用手機(jī)銀行的時(shí)候經(jīng)常能看到APP上會將銀行卡的卡號中間部分給隱藏掉使用 ***** 來代替,在某些網(wǎng)站上查看一些業(yè)務(wù)密碼時(shí)(例如簽到密碼等)也會使用 ***** 來隱藏掉真正的密碼,那么這種方式是如何實(shí)現(xiàn)的呢?
Hutool
Hutool是一個小而全的Java工具類庫,通過靜態(tài)方法封裝,降低相關(guān)API的學(xué)習(xí)成本,提高工作效率,使Java擁有函數(shù)式語言般的優(yōu)雅,讓Java語言也可以“甜甜的”。
Hutool中的工具方法來自每個用戶的精雕細(xì)琢,它涵蓋了Java開發(fā)底層代碼中的方方面面,它既是大型項(xiàng)目開發(fā)中解決小問題的利器,也是小型項(xiàng)目中的效率擔(dān)當(dāng);
Hutool是項(xiàng)目中 util 包友好的替代,它節(jié)省了開發(fā)人員對項(xiàng)目中公用類和公用工具方法的封裝時(shí)間,使開發(fā)專注于業(yè)務(wù),同時(shí)可以最大限度的避免封裝不完善帶來的bug。
我們這篇文章的實(shí)現(xiàn)思路就基于Hutool來實(shí)現(xiàn),在Hutool中提供了一個名為 DesensitizedUtil 的工具類,我們使用這個工具類來加密。
首先我們先來看一下這個類里的具體實(shí)現(xiàn),如下:

我們可以看到映入眼簾的除了一個無參構(gòu)造之外就是一個名為 desensitized 的方法,這個方法就是我們加密的主要方法,里面利用了 switch…case 方法來區(qū)分不同的加密方法。我們可以來寫一個單元測試來測試一下通過這個方法加密后是什么樣的。

以上為加密后的信息,里面我使用了不同的類型來進(jìn)行加密,目前最新版的Hutool支持脫敏加密的類型如下:
- 用戶ID
- 中文名
- 密碼
- 地址
- 郵箱
- 座機(jī)號
- 手機(jī)號
- 中國大陸的車牌號
- 銀行卡號
- IPv4地址
- IPv6地址
- 自定義脫敏
實(shí)現(xiàn)
通過以上的示例我們就可以開始編寫我們自己的脫敏操作了,首先我們要先根據(jù)以上Hutool中提供的脫敏類型來編寫我們自己的類型**(如嫌麻煩也可省略此步驟,直接使用DesensitizedUtil中的DesensitizedType)**
編寫數(shù)據(jù)脫敏類型
/**
* @author Bummon
* @description 數(shù)據(jù)脫敏策略
* @date 2023-09-01 17:43
*/
public enum DataMaskingType {
/**
* 用戶ID
*/
USER_ID,
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份證號
*/
ID_CARD,
/**
* 座機(jī)
*/
FIXED_PHONE,
/**
* 手機(jī)號
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 郵箱
*/
EMAIL,
/**
* 密碼
*/
PASSWORD,
/**
* 中國大陸車牌號
*/
CAR_LICENSE,
/**
* 銀行卡號
*/
BANK_CARD,
/**
* IPv4地址
*/
IPV4,
/**
* IPv6地址
*/
IPV6,
/**
* 自定義類型
*/
CUSTOM;
}編寫自定義注解
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 數(shù)據(jù)脫敏自定義注解
* @date 2023-09-01 18:01
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DataMaskingSerialize.class)
public @interface DataMasking {
/**
* 數(shù)據(jù)脫敏類型
*/
DataMaskingType type() default DataMaskingType.CUSTOM;
/**
* 脫敏開始位置(包含)
*/
int start() default 0;
/**
* 脫敏結(jié)束位置(不包含)
*/
int end() default 0;
}需要注意的是:當(dāng)DataMaskingType為 CUSTOM 時(shí),才需要填寫 start 和 end ,且這兩個參數(shù)才會生效,且 start 中是包含當(dāng)前下標(biāo)的字符的,而 end 不包含當(dāng)前下標(biāo)的字符。
編寫自定義序列化類
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;
//座機(jī)
case FIXED_PHONE:
jsonGenerator.writeString(DesensitizedUtil.desensitized(value, DesensitizedUtil.DesensitizedType.FIXED_PHONE));
break;
//手機(jī)號
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 并實(shí)現(xiàn)了 ContextualSerializer 中的方法,并對我們自定義注解聲明的字段進(jìn)行攔截和脫敏加密操作,接下來我們可以來測試一下效果。
測試
因?yàn)槭菍?shí)例化的時(shí)候才會被脫敏,那我們就創(chuàng)建一個實(shí)體類來存放我們需要加密的信息。
編寫測試實(shí)體類
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;
/**
* 不進(jìn)行數(shù)據(jù)脫敏的字段
*/
private String noMask;
}編寫測試Controller
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("河南省鄭州市中原區(qū)")
.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("我是不需要數(shù)據(jù)脫敏的字段")
.build();
}
}接下來我們啟動項(xiàng)目來看測試一下得到的是否為我們預(yù)期的數(shù)據(jù):

我們可以看到,我們加了注解的字段都被正確的脫敏了,而沒加注解的字段會正常顯示。
總結(jié)
我們使用了Hutool的DesensitizedUtil中的 desensitized 方法來實(shí)現(xiàn)數(shù)據(jù)脫敏,在 CUSTOM 類型的脫敏字段中,start 和 end 兩個屬性是必填的,且 start 包含當(dāng)前下標(biāo),而 end 不包含當(dāng)前下標(biāo)。
以上就是Java使用Hutool+自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏的詳細(xì)內(nèi)容,更多關(guān)于Hutool+自定義注解數(shù)據(jù)脫敏的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring?Boot面試必問之啟動流程知識點(diǎn)詳解
SpringBoot是Spring開源組織下的子項(xiàng)目,是Spring組件一站式解決方案,主要是簡化了使用Spring的難度,簡省了繁重的配置,提供了各種啟動器,開發(fā)者能快速上手,這篇文章主要給大家介紹了關(guān)于Spring?Boot面試必問之啟動流程知識點(diǎn)的相關(guān)資料,需要的朋友可以參考下2022-06-06
mybatis 如何返回list<String>類型數(shù)據(jù)
這篇文章主要介紹了mybatis 如何返回list<String>類型數(shù)據(jù)方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10
SpringSecurity+jwt+redis基于數(shù)據(jù)庫登錄認(rèn)證的實(shí)現(xiàn)
本文主要介紹了SpringSecurity+jwt+redis基于數(shù)據(jù)庫登錄認(rèn)證的實(shí)現(xiàn),其中也涉及到自定義的過濾器和處理器,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09
Java 實(shí)戰(zhàn)項(xiàng)目錘煉之樸素風(fēng)格個人博客系統(tǒng)的實(shí)現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用Java+vue+Springboot+ssm+mysql+maven+redis實(shí)現(xiàn)一個樸素風(fēng)格的個人博客系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2021-11-11
如何使用@AllArgsConstructor和final 代替 @Autowired
這篇文章主要介紹了使用@AllArgsConstructor和final 代替 @Autowired方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
Spring很常用的@Conditional注解的使用場景和源碼解析
今天要分享的是Spring的注解@Conditional,@Conditional是一個條件注解,它的作用是判斷Bean是否滿足條件,本文詳細(xì)介紹了@Conditional注解的使用場景和源碼,需要的朋友可以參考一下2023-04-04
RocketMQ中的消費(fèi)模式和消費(fèi)策略詳解
這篇文章主要介紹了RocketMQ中的消費(fèi)模式和消費(fèi)策略詳解,RocketMQ 是基于發(fā)布訂閱模型的消息中間件,所謂的發(fā)布訂閱就是說,consumer 訂閱了 broker 上的某個 topic,當(dāng) producer 發(fā)布消息到 broker 上的該 topic 時(shí),consumer 就能收到該條消息,需要的朋友可以參考下2023-10-10
java之向linux文件夾下寫文件無權(quán)限的問題
這篇文章主要介紹了java之向linux文件夾下寫文件無權(quán)限的問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09

