Java使用Hutool+自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏
前言
我們?cè)谑褂檬謾C(jī)銀行的時(shí)候經(jīng)常能看到APP上會(huì)將銀行卡的卡號(hào)中間部分給隱藏掉使用 ***** 來代替,在某些網(wǎng)站上查看一些業(yè)務(wù)密碼時(shí)(例如簽到密碼等)也會(huì)使用 ***** 來隱藏掉真正的密碼,那么這種方式是如何實(shí)現(xiàn)的呢?
Hutool
Hutool是一個(gè)小而全的Java工具類庫,通過靜態(tài)方法封裝,降低相關(guān)API的學(xué)習(xí)成本,提高工作效率,使Java擁有函數(shù)式語言般的優(yōu)雅,讓Java語言也可以“甜甜的”。
Hutool中的工具方法來自每個(gè)用戶的精雕細(xì)琢,它涵蓋了Java開發(fā)底層代碼中的方方面面,它既是大型項(xiàng)目開發(fā)中解決小問題的利器,也是小型項(xiàng)目中的效率擔(dān)當(dāng);
Hutool是項(xiàng)目中 util 包友好的替代,它節(jié)省了開發(fā)人員對(duì)項(xiàng)目中公用類和公用工具方法的封裝時(shí)間,使開發(fā)專注于業(yè)務(wù),同時(shí)可以最大限度的避免封裝不完善帶來的bug。
我們這篇文章的實(shí)現(xiàn)思路就基于Hutool來實(shí)現(xiàn),在Hutool中提供了一個(gè)名為 DesensitizedUtil
的工具類,我們使用這個(gè)工具類來加密。
首先我們先來看一下這個(gè)類里的具體實(shí)現(xiàn),如下:
我們可以看到映入眼簾的除了一個(gè)無參構(gòu)造之外就是一個(gè)名為 desensitized
的方法,這個(gè)方法就是我們加密的主要方法,里面利用了 switch…case 方法來區(qū)分不同的加密方法。我們可以來寫一個(gè)單元測(cè)試來測(cè)試一下通過這個(gè)方法加密后是什么樣的。
以上為加密后的信息,里面我使用了不同的類型來進(jìn)行加密,目前最新版的Hutool支持脫敏加密的類型如下:
- 用戶ID
- 中文名
- 密碼
- 地址
- 郵箱
- 座機(jī)號(hào)
- 手機(jī)號(hào)
- 中國大陸的車牌號(hào)
- 銀行卡號(hào)
- 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, /** * 身份證號(hào) */ ID_CARD, /** * 座機(jī) */ FIXED_PHONE, /** * 手機(jī)號(hào) */ MOBILE_PHONE, /** * 地址 */ ADDRESS, /** * 郵箱 */ EMAIL, /** * 密碼 */ PASSWORD, /** * 中國大陸車牌號(hào) */ CAR_LICENSE, /** * 銀行卡號(hào) */ 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
,且這兩個(gè)參數(shù)才會(huì)生效,且 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; //身份證號(hào) 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ī)號(hào) 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; //中國大陸車牌號(hào) 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
中的方法,并對(duì)我們自定義注解聲明的字段進(jìn)行攔截和脫敏加密操作,接下來我們可以來測(cè)試一下效果。
測(cè)試
因?yàn)槭菍?shí)例化的時(shí)候才會(huì)被脫敏,那我們就創(chuàng)建一個(gè)實(shí)體類來存放我們需要加密的信息。
編寫測(cè)試實(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; }
編寫測(cè)試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(); } }
接下來我們啟動(dòng)項(xiàng)目來看測(cè)試一下得到的是否為我們預(yù)期的數(shù)據(jù):
我們可以看到,我們加了注解的字段都被正確的脫敏了,而沒加注解的字段會(huì)正常顯示。
總結(jié)
我們使用了Hutool的DesensitizedUtil中的 desensitized
方法來實(shí)現(xiàn)數(shù)據(jù)脫敏,在 CUSTOM 類型的脫敏字段中,start
和 end
兩個(gè)屬性是必填的,且 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ù)脫敏的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring?Boot面試必問之啟動(dòng)流程知識(shí)點(diǎn)詳解
SpringBoot是Spring開源組織下的子項(xiàng)目,是Spring組件一站式解決方案,主要是簡(jiǎn)化了使用Spring的難度,簡(jiǎn)省了繁重的配置,提供了各種啟動(dòng)器,開發(fā)者能快速上手,這篇文章主要給大家介紹了關(guān)于Spring?Boot面試必問之啟動(dòng)流程知識(shí)點(diǎn)的相關(guān)資料,需要的朋友可以參考下2022-06-06mybatis 如何返回list<String>類型數(shù)據(jù)
這篇文章主要介紹了mybatis 如何返回list<String>類型數(shù)據(jù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10SpringSecurity+jwt+redis基于數(shù)據(jù)庫登錄認(rèn)證的實(shí)現(xiàn)
本文主要介紹了SpringSecurity+jwt+redis基于數(shù)據(jù)庫登錄認(rèn)證的實(shí)現(xiàn),其中也涉及到自定義的過濾器和處理器,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09Java 實(shí)戰(zhàn)項(xiàng)目錘煉之樸素風(fēng)格個(gè)人博客系統(tǒng)的實(shí)現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用Java+vue+Springboot+ssm+mysql+maven+redis實(shí)現(xiàn)一個(gè)樸素風(fēng)格的個(gè)人博客系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2021-11-11如何使用@AllArgsConstructor和final 代替 @Autowired
這篇文章主要介紹了使用@AllArgsConstructor和final 代替 @Autowired方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09Spring很常用的@Conditional注解的使用場(chǎng)景和源碼解析
今天要分享的是Spring的注解@Conditional,@Conditional是一個(gè)條件注解,它的作用是判斷Bean是否滿足條件,本文詳細(xì)介紹了@Conditional注解的使用場(chǎng)景和源碼,需要的朋友可以參考一下2023-04-04RocketMQ中的消費(fèi)模式和消費(fèi)策略詳解
這篇文章主要介紹了RocketMQ中的消費(fèi)模式和消費(fèi)策略詳解,RocketMQ 是基于發(fā)布訂閱模型的消息中間件,所謂的發(fā)布訂閱就是說,consumer 訂閱了 broker 上的某個(gè) topic,當(dāng) producer 發(fā)布消息到 broker 上的該 topic 時(shí),consumer 就能收到該條消息,需要的朋友可以參考下2023-10-10java之向linux文件夾下寫文件無權(quán)限的問題
這篇文章主要介紹了java之向linux文件夾下寫文件無權(quán)限的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09