據(jù)脫敏)
順手再加個敏感數(shù)據(jù)脫敏上一篇做完了字典翻譯這套 SPI 還剩一個很劃算的用途敏感數(shù)據(jù)脫敏。引擎是共用的這篇只需要寫 5 個文件。沒看過上一篇的話先把那四個json包的文件JsonValueEnhancer、JsonFieldProcessor、JsonFieldContext、JsonEnhancementContext和TranslationConfig裝進(jìn)項目。這篇只講脫敏部分。問題接口返回{empName:張三,phone:13812345678,idCard:310101199003071234}手機(jī)號、身份證直接裸露。放前端脫敏沒用——接口一調(diào)全出來了必須在后端處理。常見做法是每個 VO 里手動調(diào)一遍StringUtils.mask(phone)跟字典翻譯面臨一樣的問題到處是重復(fù)代碼還容易漏。我們想要的是{empName:張*,phone:138****5678,idCard:310101********1234}VO 上打個注解就行Sensitive(typeSensitiveType.PHONE)privateStringphone;依賴不用加一個都不用。完全復(fù)用上一篇的引擎也沒有 Hutool 依賴脫敏算法自己實現(xiàn)。和字典翻譯的區(qū)別搞清楚這三點代碼就很好懂字典翻譯脫敏要不要查外部數(shù)據(jù)要查字典不要純字符串處理要不要批量預(yù)取要防 N1不要用不上 collect/prepare讀哪個字段的值一般用mapper讀別的字段就讀自己這個字段不需要mapper所以這邊只需要實現(xiàn)supports()和process()另外兩個用接口的默認(rèn)空實現(xiàn)。設(shè)計每個脫敏類型自帶一個函數(shù)不要寫成一個大switch——那樣每加一種脫敏方式都要回來改處理器。用策略模式SensitiveStrategy 接口就一個方法FunctionString, String desensitizer() ▲ │ implements SensitiveType 枚舉每個枚舉項自帶脫敏函數(shù) SensitiveJsonFieldProcessor 處理器只管取注解然后 apply不管具體怎么脫加一種脫敏方式 加一個枚舉項。處理器一行都不用改。代碼包名cn.prohup.xiao.crm.commons.sensitivecommons/sensitive/ ├── annotation/ Sensitive.java ├── core/ │ ├── SensitiveStrategy.java 策略接口 │ ├── SensitiveType.java 枚舉實現(xiàn)策略 │ └── util/ │ └── SensitiveUtil.java 脫敏小工具 ├── handler/ SensitiveJsonFieldProcessor.java └── config/ SensitiveConfig.java1. 注解annotation/Sensitive.javapackagecn.prohup.xiao.crm.commons.sensitive.annotation;importcn.prohup.xiao.crm.commons.sensitive.core.SensitiveType;importjava.lang.annotation.Documented;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/** * type 脫敏類型 * beginIndex CUSTOM 模式保留的前綴位數(shù) * endIndex CUSTOM 模式保留的后綴位數(shù) */Retention(RetentionPolicy.RUNTIME)Target({ElementType.FIELD,ElementType.METHOD})DocumentedpublicinterfaceSensitive{SensitiveTypetype()defaultSensitiveType.CUSTOM;intbeginIndex()default0;intendIndex()default0;}2. 策略接口和工具類core/SensitiveStrategy.javapackagecn.prohup.xiao.crm.commons.sensitive.core;importjava.util.function.Function;/** * 脫敏策略一個策略就是一個字符串轉(zhuǎn)換函數(shù)入?yún)⒃某鰠⒚撁艉蟮奈谋尽?* * 想自己擴(kuò)展的話實現(xiàn)這個接口即可處理器不用動。 * 也可以自定義一個 Function 直接塞給 SensitiveType見 SensitiveType.CUSTOM 的寫法。 */publicinterfaceSensitiveStrategy{/** 返回該策略對應(yīng)的脫敏函數(shù) */FunctionString,Stringdesensitizer();}core/util/SensitiveUtil.javapackagecn.prohup.xiao.crm.commons.sensitive.core.util;importjava.util.Arrays;publicfinalclassSensitiveUtil{privatestaticfinalcharMASK*;privateSensitiveUtil(){}/** * 保留前 prefix 位和后 suffix 位中間全部填 *。 * 位數(shù)按實際長度算不是固定幾顆星。 */publicstaticStringkeep(Stringstr,intprefix,intsuffix){if(strnull||str.isEmpty()){returnstr;}intlenstr.length();// 短到前后都要塌陷的地步直接全遮if(lenprefixsuffix){returnrepeat(len);}StringBuildersbnewStringBuilder();sb.append(str.substring(0,prefix));sb.append(repeat(len-prefix-suffix));sb.append(str.substring(len-suffix));returnsb.toString();}/** 只留第一個字張三 → 張* */publicstaticStringname(Stringstr){if(strnull||str.length()1){returnstr;}returnstr.substring(0,1)repeat(str.length()-1);}/** 保留首位和 之后的完整域名zhangsan163.com → z*******163.com */publicstaticStringemail(Stringstr){if(strnull||str.isEmpty()){returnstr;}intatstr.indexOf();if(at0){returnkeep(str,1,0);}StringBuildersbnewStringBuilder();sb.append(str.substring(0,1));sb.append(repeat(at-1));sb.append(str.substring(at));returnsb.toString();}publicstaticStringrepeat(intcount){if(count0){return;}char[]arrnewchar[count];Arrays.fill(arr,MASK);returnnewString(arr);}}3. 枚舉實現(xiàn)策略core/SensitiveType.java每個枚舉項直接把自己對應(yīng)的函數(shù)寫進(jìn)去。想加新的就加一行。packagecn.prohup.xiao.crm.commons.sensitive.core;importcn.prohup.xiao.crm.commons.sensitive.core.util.SensitiveUtil;importjava.util.function.Function;/** * 內(nèi)置脫敏類型每個枚舉項自帶脫敏函數(shù)。 * 加一種脫敏方式 加一個枚舉項處理器完全不用動。 */publicenumSensitiveTypeimplementsSensitiveStrategy{/** 手機(jī)138****5678保留前 3 后 4 */PHONE(s-SensitiveUtil.keep(s,3,4)),/** 身份證保留前 6 后 4 */ID_CARD(s-SensitiveUtil.keep(s,6,4)),/** 銀行卡只留后 4 位 */BANK_CARD(s-SensitiveUtil.keep(s,0,4)),/** 地址保留前 6 個字 */ADDRESS(s-SensitiveUtil.keep(s,6,0)),/** 姓名只留第一個字 */NAME(SensitiveUtil::name),/** 郵箱保留首位和域名 */EMAIL(SensitiveUtil::email),/** 自定義位數(shù)由注解上的 beginIndex / endIndex 決定處理器里單獨處理 */CUSTOM(s-s);/** 可自行添加其他脫敏策略比如護(hù)照 s - SensitiveUtil.keep(s, 2, 3) */privatefinalFunctionString,Stringdesensitizer;SensitiveType(FunctionString,Stringdesensitizer){this.desensitizerdesensitizer;}OverridepublicFunctionString,Stringdesensitizer(){returndesensitizer;}}CUSTOM為什么是s - s原樣返回因為它的位數(shù)來自注解參數(shù)沒法在枚舉里預(yù)先定死得在處理器里特判。下面會看到。4. 處理器handler/SensitiveJsonFieldProcessor.javaOrder(10)是執(zhí)行順序數(shù)字越大越靠后。脫敏通常安排在翻譯之后——萬一某字段既翻譯又要脫敏先拿到可讀文本再脫敏。packagecn.prohup.xiao.crm.commons.sensitive.handler;importcn.prohup.xiao.crm.commons.sensitive.annotation.Sensitive;importcn.prohup.xiao.crm.commons.sensitive.core.SensitiveType;importcn.prohup.xiao.crm.commons.sensitive.core.util.SensitiveUtil;importcn.prohup.xiao.crm.commons.translation.json.JsonEnhancementContext;importcn.prohup.xiao.crm.commons.translation.json.JsonFieldContext;importcn.prohup.xiao.crm.commons.translation.json.JsonFieldProcessor;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.core.annotation.Order;/** * 敏感字段脫敏。不需要 collect / prepare純字符串處理沒有批量查詢的必要。 * 具體的脫敏邏輯都掛在 SensitiveType 上這里只負(fù)責(zé)取注解、調(diào)函數(shù)。 */Order(10)publicclassSensitiveJsonFieldProcessorimplementsJsonFieldProcessor{privatestaticfinalLoggerlogLoggerFactory.getLogger(SensitiveJsonFieldProcessor.class);Overridepublicbooleansupports(JsonFieldContextfieldContext){returnfieldContext.getAnnotation(Sensitive.class)!null;}OverridepublicObjectprocess(JsonFieldContextfieldContext,Objectvalue,JsonEnhancementContextcontext){SensitivesensitivefieldContext.getAnnotation(Sensitive.class);if(sensitivenull){returnvalue;}// 只處理字符串?dāng)?shù)字、日期、對象一律放過if(!(valueinstanceofString)){returnvalue;}Stringstr(String)value;if(str.isEmpty()){returnstr;}try{SensitiveTypetypesensitive.type();// CUSTOM 的位數(shù)來自注解不能預(yù)先定成一個函數(shù)單獨走這里if(typeSensitiveType.CUSTOM){returnSensitiveUtil.keep(str,sensitive.beginIndex(),sensitive.endIndex());}// 其余類型直接取枚舉自帶函數(shù)執(zhí)行returntype.desensitizer().apply(str);}catch(Exceptione){// 脫敏出錯寧可返回原文也不能讓整個接口掛掉log.error(脫敏異常 field{},fieldContext.propertyName(),e);returnvalue;}}}5. 裝配config/SensitiveConfig.java只需要注冊處理器 Bean。引擎不用再注冊一遍——上一篇TranslationConfig里的JsonValueEnhancer會自動把容器里所有JsonFieldProcessor收集進(jìn)去脫敏這個自然掛上去了。packagecn.prohup.xiao.crm.commons.sensitive.config;importcn.prohup.xiao.crm.commons.sensitive.handler.SensitiveJsonFieldProcessor;importorg.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;ConfigurationpublicclassSensitiveConfig{/** * 引擎JsonValueEnhancer在上一篇的 TranslationConfig 里已經(jīng)注冊好了 * 它通過 Listlt;JsonFieldProcessorgt; 自動收集所有處理器這里只要把自己交給 Spring 即可。 */BeanConditionalOnMissingBeanpublicSensitiveJsonFieldProcessorsensitiveJsonFieldProcessor(){returnnewSensitiveJsonFieldProcessor();}}跑起來VO 加注解DatapublicclassEmployeeVo{privateLongempId;Sensitive(typeSensitiveType.NAME)privateStringempName;Sensitive(typeSensitiveType.PHONE)privateStringphone;Sensitive(typeSensitiveType.ID_CARD)privateStringidCard;Sensitive(typeSensitiveType.EMAIL)privateStringemail;Sensitive(typeSensitiveType.BANK_CARD)privateStringbankCard;/** 保留前 2 后 2中間全遮 */Sensitive(typeSensitiveType.CUSTOM,beginIndex2,endIndex2)privateStringremark;}拿這組數(shù)據(jù)跑張三/13812345678/310101199003071234zhangsan163.com/6222020200011234/客戶溝通很滿意實際返回這組值是真跑出來的不是手寫示意{empId:1,empName:張*,phone:138****5678,idCard:310101********1234,email:z*******163.com,bankCard:************1234,remark:客戶***滿意}注意remark是 7 個字客戶溝通很滿意保留前 2 后 2中間 3 個字全遮——keep()按實際長度算位數(shù)不是固定幾顆星。empId是 Long 沒被處理符合只處理字符串。列表、嵌套對象、MapString, Object返回值統(tǒng)統(tǒng)生效因為遞歸是引擎做的。某個接口想看明文比如管理員才可見的詳情頁別用帶注解的 VO另定義一個不加密注解的 VO 返回即可。加一種脫敏策略只要兩步都在這個包里處理器不用動。第一步SensitiveUtil里如果沒有現(xiàn)成方法加一個/** 護(hù)照保留前 2 后 3 */publicstaticStringpassport(Stringstr){returnkeep(str,2,3);}keep已經(jīng)能覆蓋大部分情況所以這步經(jīng)常可以跳過。第二步枚舉里加一項/** 護(hù)照保留前 2 后 3 */PASSPORT(s-SensitiveUtil.keep(s,2,3)),完事。VO 里就能用Sensitive(type SensitiveType.PASSPORT)了。一個容易踩的坑策略類直接用不了你可能會想單獨寫一個策略類publicclassPhoneTailStrategyimplementsSensitiveStrategy{OverridepublicFunctionString,Stringdesensitizer(){// 長度夠 4 位4 顆星 后 4 位不夠就全遮否則 substring 會越界returns-s.length()4?****s.substring(s.length()-4):SensitiveUtil.repeat(s.length());}}它單獨調(diào)用是沒問題的newPhoneTailStrategy().desensitizer().apply(13812345678);// ****5678但它接不到Sensitive注解上——注解里type()的類型是SensitiveType枚舉填不進(jìn)一個 classSensitive(type PhoneTailStrategy.class)編譯不過。想讓它生效最省事的辦法是把 lambda 直接寫成枚舉項/** 手機(jī)尾號只顯示后 4 位前面補(bǔ) **** */PHONE_TAIL(s-s.length()4?****s.substring(s.length()-4):SensitiveUtil.repeat(s.length())),然后 VO 里就能用Sensitive(typeSensitiveType.PHONE_TAIL)privateStringphone;// 13812345678 → ****5678結(jié)論SensitiveStrategy接口的價值在于「枚舉項里可以掛任意 lambda」而不是讓你在外面 new 一個策略類。真要做到可插拔比如讓第三方 jar 也能加策略注解得改成Class? extends SensitiveStrategy strategy()、處理器里再反射實例化——復(fù)雜度上去了一般業(yè)務(wù)用不上枚舉項足夠??蝇F(xiàn)象原因脫敏沒生效只處理 String。數(shù)字、日期、Long 不會脫敏需要脫敏的類型得先轉(zhuǎn)成 String報非法 rangebeginIndex endIndex超過字符串長度keep()里已做全遮兜底和字典翻譯互相覆蓋同一字段別同時打兩個注解打架。真需要的話順序是翻譯先Order(0)、脫敏后Order(10)加了注解但值是 null直接返回不報錯導(dǎo)出 Excel 沒脫敏本文只處理 JSON 響應(yīng)導(dǎo)出走的是另一條路要在導(dǎo)出邏輯里單獨處理和上一篇共用的驗收列表 100 條字典查詢?nèi)罩局怀霈F(xiàn) 1 次。加了脫敏之后這條還得成立——脫敏本身沒有任何 IO如果變慢了說明處理器里混進(jìn)了查詢操作。兩套能力加起來二十來個文件共用同一個引擎。想再加第三種比如給 OSS 的 fileId 自動補(bǔ)成完整訪問 URL也是同一個套路寫個實現(xiàn)JsonFieldProcessor的類supports()判斷注解process()做轉(zhuǎn)換注冊成 Bean 就完事。