SpringBoot使用自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏過程詳細(xì)解析
前言
對于某些接口返回的信息,涉及到敏感數(shù)據(jù)的必須進(jìn)行脫敏操作,例如銀行卡號、身份證號、手機(jī)號等,脫敏方式有多種方式??梢孕薷腟QL語句,也可以寫硬代碼,也可以修改JSON序列化,這里介紹通過修改Jackson序列化方式實(shí)現(xiàn)數(shù)據(jù)脫敏。
一、引入hutool工具類
maven:
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.5</version> </dependency>
gradle:
// https://mvnrepository.com/artifact/cn.hutool/hutool-all
implementation group: 'cn.hutool', name: 'hutool-all', version: '5.8.5'
二、定義常用需要脫敏的數(shù)據(jù)類型的枚舉
其中 OTHER類型為自定義類型,需在后面自定義脫敏的長度等。
package com.iscas.authentication.model.enums; import lombok.Getter; /** * * @version 1.0 * @since jdk1.8 */ @Getter public enum PrivacyTypeEnum { /** * 中文名 * */ CHINESE_NAME, /** * 固話 * */ FIXED_PHONE, /** * 手機(jī)號 * */ MOBILE_PHONE, /** * 住址 * */ ADDRESS, /** * 密碼 * */ PASSWORD, /** * 銀行卡號 * */ BANK_CARD, /** * 郵箱 * */ EMAIL, /** * 身份證 * */ ID_CARD, /** * 其他類型 * */ OTHER; }
三、定義脫敏方式枚舉
其中,DEFAULT類型時,需要數(shù)據(jù)類型為上一步枚舉中除OTHER外的已確定的類型,NONE表示不做脫敏,其他類型為注釋的意思。
package com.iscas.authentication.model.enums; /** * * @version 1.0 * @since jdk1.8 */ public enum DesensitizationTypeEnum { /** * 默認(rèn)方式 * */ DEFAULT, /** * 頭部脫敏 * */ HEAD, /** * 尾部脫敏 * */ TAIL, /** * 中間脫敏 * */ MIDDLE, /** * 頭尾脫敏 * */ HEAD_TAIL, /** * 全部脫敏 * */ ALL, /** * 不脫敏,相當(dāng)于沒打這個注解 * */ NONE; }
四、自定義脫敏的注解
其中,mode默認(rèn)為DEFAULT,此時只需要設(shè)置dataType的類型為除OTHER外的確定類型即可,當(dāng)mode不是DEFAULT或NONE時,根據(jù)不同的類型,headNoMaskLen等長度屬性需要設(shè)置,見上面的注釋的字面意思。
package com.iscas.authentication.annotation; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.iscas.authentication.model.enums.DesensitizationTypeEnum; import com.iscas.authentication.model.enums.PrivacyTypeEnum; import com.iscas.authentication.service.DesensitizationSerializer; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 脫敏注解 * * @version 1.0 * @since jdk1.8 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @JsonSerialize(using = DesensitizationSerializer.class) public @interface Desensitization { /** * 脫敏的隱私數(shù)據(jù)類型 */ PrivacyTypeEnum dataType(); /** * 脫敏方式,默認(rèn)方式不需要定義下面脫敏長度等信息,根據(jù)脫敏的隱私數(shù)據(jù)類型自動脫敏 */ DesensitizationTypeEnum mode() default DesensitizationTypeEnum.DEFAULT; /** * 尾部不脫敏的長度,當(dāng)mode為HEAD或MIDDLE時使用 */ int tailNoMaskLen() default 1; /** * 頭部不脫敏的長度,當(dāng)mode為TAIL或MIDDLE時使用 */ int headNoMaskLen() default 1; /** * 中間不脫敏的長度,當(dāng)mode為HEAD_TAIL時使用 */ int middleNoMaskLen() default 1; /** * 打碼 */ char maskCode() default '*'; }
五、自定義Jackson的序列化方式
package com.iscas.authentication.service; import cn.hutool.core.util.DesensitizedUtil; import cn.hutool.core.util.StrUtil; 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 com.iscas.authentication.annotation.Desensitization; import com.iscas.authentication.model.enums.DesensitizationTypeEnum; import com.iscas.authentication.model.enums.PrivacyTypeEnum; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import java.io.IOException; import java.util.Objects; /** * 脫敏序列化類 * * @author zhuquanwen * @version 1.0 * @date 2023/1/5 9:24 * @since jdk1.8 */ @AllArgsConstructor @NoArgsConstructor public class DesensitizationSerializer extends JsonSerializer<String> implements ContextualSerializer { private Desensitization desensitization; @Override public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeString(desensitize(s)); } @Override public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { if (beanProperty != null) { if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class); if (desensitization == null) { desensitization = beanProperty.getContextAnnotation(Desensitization.class); } if (desensitization != null) { return new DesensitizationSerializer(desensitization); } } return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } return serializerProvider.findNullValueSerializer(null); } /** * 脫敏處理 * */ private String desensitize(String s) { if (StrUtil.isNotBlank(s)) { PrivacyTypeEnum dataType = desensitization.dataType(); DesensitizationTypeEnum mode = desensitization.mode(); switch (mode) { case DEFAULT: // 默認(rèn)方式,根據(jù)dataType自動選擇脫敏方式 s = autoDesensitize(s, dataType); break; case HEAD: // 頭部脫敏 s = headDesensitize(s); break; case TAIL: // 尾部脫敏 s = tailDesensitize(s); break; case MIDDLE: s = middleDesensitize(s); break; case HEAD_TAIL: s = headTailDesensitize(s); break; case ALL: s = allDesensitize(s); break; case NONE: // 不做脫敏 break; default: } } return s; } /** * 全部脫敏 * */ private String allDesensitize(String s) { return String.valueOf(desensitization.maskCode()).repeat(s.length()); } /** * 頭尾脫敏 * */ private String headTailDesensitize(String s) { int middleNoMaskLen = desensitization.middleNoMaskLen(); if (middleNoMaskLen >= s.length()) { // 如果中間不脫敏的長度大于等于字符串的長度,不進(jìn)行脫敏 return s; } int len = s.length() - middleNoMaskLen; // 頭部脫敏 int headStart = 0; int headEnd = len / 2; s = StrUtil.replace(s, headStart, headEnd, desensitization.maskCode()); // 尾部脫敏 int tailStart = s.length() - (len - len / 2); int tailEnd = s.length(); return StrUtil.replace(s, tailStart, tailEnd, desensitization.maskCode()); } /** * 中間脫敏 * */ private String middleDesensitize(String s) { int headNoMaskLen = desensitization.headNoMaskLen(); int tailNoMaskLen = desensitization.tailNoMaskLen(); if (headNoMaskLen + tailNoMaskLen >= s.length()) { // 如果頭部不脫敏的長度+尾部不脫敏長度 大于等于字符串的長度,不進(jìn)行脫敏 return s; } int start = headNoMaskLen; int end = s.length() - tailNoMaskLen; return StrUtil.replace(s, start, end, desensitization.maskCode()); } /** * 尾部脫敏 * */ private String tailDesensitize(String s) { int headNoMaskLen = desensitization.headNoMaskLen(); if (headNoMaskLen >= s.length()) { // 如果頭部不脫敏的長度大于等于字符串的長度,不進(jìn)行脫敏 return s; } int start = headNoMaskLen; int end = s.length(); return StrUtil.replace(s, start, end, desensitization.maskCode()); } /** * 頭部脫敏 * */ private String headDesensitize(String s) { int tailNoMaskLen = desensitization.tailNoMaskLen(); if (tailNoMaskLen >= s.length()) { // 如果尾部不脫敏的長度大于等于字符串的長度,不進(jìn)行脫敏 return s; } int start = 0; int end = s.length() - tailNoMaskLen; return StrUtil.replace(s, start, end, desensitization.maskCode()); } public static void main(String[] args) { System.out.println(StrUtil.replace("231085198901091813", 2, -10, '#')); } /** * 根據(jù)數(shù)據(jù)類型自動脫敏 * */ private String autoDesensitize(String s, PrivacyTypeEnum dataType) { switch (dataType) { case CHINESE_NAME: s = DesensitizedUtil.chineseName(s); break; case FIXED_PHONE: s = DesensitizedUtil.fixedPhone(s); break; case MOBILE_PHONE: s = DesensitizedUtil.mobilePhone(s); break; case ADDRESS: s = DesensitizedUtil.address(s, 8); break; case PASSWORD: s = DesensitizedUtil.password(s); break; case BANK_CARD: s = DesensitizedUtil.bankCard(s); break; case EMAIL: s = DesensitizedUtil.email(s); break; case ID_CARD: s = DesensitizedUtil.idCardNum(s, 1, 2); break; case OTHER: // 其他類型的不支持以默認(rèn)方式脫敏,直接返回 break; default: } return s; } }
六、使用
下面是一個測試的例子:
package com.iscas.base.biz.test.controller; import com.iscas.base.biz.desensitization.Desensitization; import com.iscas.base.biz.desensitization.DesensitizationTypeEnum; import com.iscas.base.biz.desensitization.PrivacyTypeEnum; import lombok.Data; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; /** * * @author zhuquanwen * @version 1.0 * @date 2023/1/6 8:40 * @since jdk1.8 */ @RestController @RequestMapping("/test/desensitization") public class TestDesensitizationController { @GetMapping public List<TestModel> test() { TestModel t1 = new TestModel(); t1.setPassword("123456"); t1.setEmail("zzz@163.com"); t1.setPhone("137654879451"); t1.setFixPhone("0453-4785462"); t1.setBankCard("622648754896457"); t1.setIdCard("245874563214578965"); t1.setName("張王釗"); t1.setAddress("北京市昌平區(qū)xxx街道xxx小區(qū)1-1-101"); t1.setHeadStr("測試頭部脫敏"); t1.setTailStr("測試尾部脫敏"); t1.setMiddleStr("測試中間脫敏"); t1.setHeadTailStr("測試頭尾脫敏"); t1.setAllStr("測試全部脫敏"); t1.setNoneStr("測試不脫敏"); TestModel t2 = new TestModel(); t2.setPassword("iscas123"); t2.setEmail("xwg@sina.com"); t2.setPhone("18547896547"); t2.setFixPhone("010-62268795"); t2.setBankCard("622648754896487"); t2.setIdCard("100412547865478947"); t2.setName("李二麻子"); t2.setAddress("新疆省克拉瑪依市xxx街道xxx小區(qū)1-1-101"); t2.setHeadStr("測試頭部脫敏"); t2.setTailStr("測試尾部脫敏"); t2.setMiddleStr("測試中間脫敏"); t2.setHeadTailStr("測試頭尾脫敏"); t2.setAllStr("測試全部脫敏"); t2.setNoneStr("測試不脫敏"); return new ArrayList<>(){{ add(t1); add(t2); }}; } @Data private static class TestModel { /** * 模擬密碼 * */ @Desensitization(dataType = PrivacyTypeEnum.PASSWORD) private String password; /** * 模擬郵箱 * */ @Desensitization(dataType = PrivacyTypeEnum.EMAIL) private String email; /** * 模擬手機(jī)號 * */ @Desensitization(dataType = PrivacyTypeEnum.MOBILE_PHONE) private String phone; /** * 模擬座機(jī) * */ @Desensitization(dataType = PrivacyTypeEnum.FIXED_PHONE) private String fixPhone; /** * 模擬銀行卡 * */ @Desensitization(dataType = PrivacyTypeEnum.BANK_CARD) private String bankCard; /** * 模擬身份證號 * */ @Desensitization(dataType = PrivacyTypeEnum.ID_CARD) private String idCard; /** * 模擬中文名 * */ @Desensitization(dataType = PrivacyTypeEnum.CHINESE_NAME) private String name; /** * 模擬住址 * */ @Desensitization(dataType = PrivacyTypeEnum.ADDRESS) private String address; /** * 模擬自定義脫敏-頭部脫敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.HEAD, tailNoMaskLen = 4) private String headStr; /** * 模擬自定義脫敏-尾部脫敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.TAIL, headNoMaskLen = 4) private String tailStr; /** * 模擬自定義脫敏-中間脫敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.MIDDLE, headNoMaskLen = 2, tailNoMaskLen = 2) private String middleStr; /** * 模擬自定義脫敏-兩頭脫敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.HEAD_TAIL, middleNoMaskLen = 4) private String headTailStr; /** * 模擬自定義脫敏-全部脫敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.ALL) private String allStr; /** * 模擬自定義脫敏-不脫敏 * */ @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.NONE) private String noneStr; } }
下面是一個實(shí)際使用的例子如下,在tel、password、email上添加了@Desensitization注解,自定義的@TbField等注解請忽略
package com.iscas.authentication.model.sys; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.iscas.authentication.annotation.Desensitization; import com.iscas.authentication.model.enums.PrivacyTypeEnum; import com.iscas.templet.annotation.table.TbField; import com.iscas.templet.annotation.table.TbFieldRule; import com.iscas.templet.annotation.table.TbSetting; import com.iscas.templet.view.table.TableFieldType; import com.iscas.templet.view.table.TableSearchType; import com.iscas.templet.view.table.TableViewType; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.experimental.Accessors; import java.util.List; /** * @author zhuquanwen * @version 1.0 * @date 2022/3/11 21:23 * @since jdk11 */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Schema(title = "用戶") @TableName(value = "oauth_sys_user") @Accessors(chain = true) @TbSetting(title = "用戶", checkbox = true, viewType = TableViewType.multi) public class User extends BaseEntity { @TableId(type = IdType.AUTO) @Schema(title = "id") @TbField(field = "id", header = "id", type = TableFieldType.text, hidden = true) private Integer id; @Schema(title = "用戶名") @TbField(field = "name", header = "名稱", search = true, searchType = TableSearchType.like, type = TableFieldType.text, rule=@TbFieldRule(required = true, minLength = 2, maxLength = 20, distinct = true, desc = "用戶名不能為空,且長度介于2-20個字符之間")) private String name; @Schema(title = "密碼") @TbField(field = "password", header = "密碼", hidden = true, editable = false, type = TableFieldType.text) @Desensitization(dataType = PrivacyTypeEnum.PASSWORD) private String password; @Schema(title = "type") @TbField(field = "type", header = "用戶類型", search = true, searchType = TableSearchType.exact, type = TableFieldType.select, option = "[{\"label\":\"正常用戶\",\"value\":\"1\"},{\"label\":\"戰(zhàn)位IP用戶\",\"value\":\"2\"}]") private String type; @Schema(title = "status") @TbField(field = "status", header = "狀態(tài)", search = true, searchType = TableSearchType.exact, type = TableFieldType.select, option = "[{\"label\":\"正常\",\"value\":\"1\"},{\"label\":\"禁用\",\"value\":\"0\"}]") private String status; @Schema(title = "真實(shí)姓名") @TbField(field = "realName", header = "真實(shí)姓名", type = TableFieldType.text, rule=@TbFieldRule(required = true, minLength = 2, maxLength = 20, desc = "真實(shí)姓名不能為空,且長度介于2-20個字符之間")) private String realName; @Schema(title = "電話號碼") @TbField(field = "tel", header = "電話號碼", type = TableFieldType.text, rule=@TbFieldRule(reg = "^(13[0-9]|14[01456879]|15[0-3,5-9]|16[2567]|17[0-8]|18[0-9]|19[0-3,5-9])\\d{8}$", desc = "電話號碼需符規(guī)則")) @Desensitization(dataType = PrivacyTypeEnum.MOBILE_PHONE) private String tel; @Schema(title = "郵箱") @TbField(field = "email", header = "郵箱", type = TableFieldType.text, rule=@TbFieldRule(reg = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*.\\w+([-.]\\w+)*$", desc = "郵箱需符規(guī)則")) @Desensitization(dataType = PrivacyTypeEnum.EMAIL) private String email; @Schema(title = "部門") @TbField(field = "orgIds", header = "部門", type = TableFieldType.multiSelect, selectUrl = "/api/v1/orgs/combobox/tree?status=1") @TableField(exist = false) private List<Integer> orgIds; @Schema(title = "角色") @TbField(field = "roleIds", header = "角色", type = TableFieldType.multiSelect, selectUrl = "/api/v1/roles/combobox?status=1") @TableField(exist = false) private List<Integer> roleIds; @Schema(title = "崗位") @TbField(field = "postIds", header = "崗位", type = TableFieldType.multiSelect, selectUrl = "/api/v1/posts/combobox?status=1") @TableField(exist = false) private List<Integer> postIds; }
七、脫敏效果
下面是測試的結(jié)果:
下面是一個查詢接口返回帶User實(shí)體的結(jié)果:
到此這篇關(guān)于SpringBoot使用自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏過程詳細(xì)解析的文章就介紹到這了,更多相關(guān)SpringBoot數(shù)據(jù)脫敏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot實(shí)現(xiàn)返回值數(shù)據(jù)脫敏的步驟詳解
- Springboot+Jackson自定義注解數(shù)據(jù)脫敏的項(xiàng)目實(shí)踐
- Springboot+Hutool自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏
- SpringBoot利用自定義注解實(shí)現(xiàn)隱私數(shù)據(jù)脫敏(加密顯示)的解決方案
- SpringBoot利用自定義json序列化器實(shí)現(xiàn)敏感字段數(shù)據(jù)脫敏詳解
- 淺析如何在SpringBoot中實(shí)現(xiàn)數(shù)據(jù)脫敏
- SpringBoot數(shù)據(jù)脫敏的實(shí)現(xiàn)示例
- SpringBoot接口返回數(shù)據(jù)脫敏(Mybatis、Jackson)
相關(guān)文章
java.lang.IllegalArgumentException:Invalid character&nb
本文介紹了java.lang.IllegalArgumentException: Invalid character found異常的解決,方法包括檢查代碼中的方法名,使用合適的HTTP請求方法常量,使用第三方HTTP庫,檢查請求URL以及使用調(diào)試和日志工具,通過這些方法,我們可以解決異常并確保網(wǎng)絡(luò)應(yīng)用程序的正常運(yùn)行2023-10-10Mybatis利用OGNL表達(dá)式處理動態(tài)sql的方法教程
這篇文章主要給大家介紹了關(guān)于Mybatis利用OGNL表達(dá)式處理動態(tài)sql的方法教程的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2017-06-06Spring之借助Redis設(shè)計一個簡單訪問計數(shù)器的示例
本篇文章主要介紹了Spring之借助Redis設(shè)計一個簡單訪問計數(shù)器的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06由ArrayList來深入理解Java中的fail-fast機(jī)制
fail-fast俗稱快速失敗,是在多線程進(jìn)行迭代操作時產(chǎn)生沖突的一種異常拋出機(jī)制,下面我們就由ArrayList來深入理解Java中的fail-fast機(jī)制.2016-05-05JVM類運(yùn)行機(jī)制實(shí)現(xiàn)原理解析
這篇文章主要介紹了JVM類運(yùn)行機(jī)制實(shí)現(xiàn)原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-12-12Mybatis Plus 字段為空值時執(zhí)行更新方法未更新解決方案
這篇文章主要介紹了Mybatis Plus 字段為空值時執(zhí)行更新方法未更新解決方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09