使用自定義Json注解實(shí)現(xiàn)輸出日志字段脫敏
自定義Json注解實(shí)現(xiàn)輸出日志字段脫敏
背景
在日志輸出的時(shí)候,有時(shí)會(huì)輸出一些用戶的敏感信息,如手機(jī)號(hào),身份證號(hào),銀行卡號(hào)等,現(xiàn)需要對(duì)這些信息在日志輸出的時(shí)候進(jìn)行脫敏處理
思路
使用fastjson的ValueFilter對(duì)帶有自定義注解的字段進(jìn)行過濾
/** * 敏感信息類型 * * @author worstEzreal * @version V1.0.0 * @date 2017/7/19 */ public enum SensitiveType { ID_CARD, BANK_CARD, PHONE }
/** * 脫敏字段注解 * * @author worstEzreal * @version V1.0.0 * @date 2017/7/19 */ @Target({ElementType.TYPE, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface SensitiveInfo { SensitiveType type(); }
/** * 日志敏感信息脫敏工具 * * @author worstEzreal * @version V1.0.0 * @date 2017/7/19 */ public class SensitiveInfoUtils { public static String toJsonString(Object object) { return JSON.toJSONString(object, getValueFilter()); } private static String desensitizePhoneOrIdCard(String num) { if (StringUtils.isBlank(num)) { return ""; } return StringUtils.left(num, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"), "***")); } private static String desensitizeBankCard(String cardNum) { if (StringUtils.isBlank(cardNum)) { return ""; } return StringUtils.left(cardNum, 4).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"), "****")); } private static final ValueFilter getValueFilter() { return new ValueFilter() { @Override public Object process(Object obj, String key, Object value) {//obj-對(duì)象 key-字段名 value-字段值 try { Field field = obj.getClass().getDeclaredField(key); SensitiveInfo annotation = field.getAnnotation(SensitiveInfo.class); if (null != annotation && value instanceof String) { String strVal = (String) value; if (StringUtils.isNotBlank(strVal)) { switch (annotation.type()) { case PHONE: return desensitizePhoneOrIdCard(strVal); case ID_CARD: return desensitizePhoneOrIdCard(strVal); case BANK_CARD: return desensitizeBankCard(strVal); default: break; } } } } catch (NoSuchFieldException e) { //找不到的field對(duì)功能沒有影響,空處理 } return value; } }; } public static void main(String[] args) { CardInfo cardInfo = new CardInfo(); cardInfo.setId("11111111111111111"); cardInfo.setCardId("6228480402564890018"); System.out.println(SensitiveInfoUtils.toJsonString(cardInfo)); } }
附CardInfo類
public class CardInfo { private String userId; private String name; @SensitiveInfo(type = SensitiveType.ID_CARD) private String certId; @SensitiveInfo(type = SensitiveType.BANK_CARD) private String cardId; private String bank; private String phone; public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCertId() { return certId; } public void setCertId(String certId) { this.certId = certId; } public String getCardId() { return cardId; } public void setCardId(String cardId) { this.cardId = cardId; } public String getBank() { return bank; } public void setBank(String bank) { this.bank = bank; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } }
java注解式脫敏
隨著互聯(lián)網(wǎng)時(shí)代普及,用戶的信息越來越重要,我們開發(fā)軟件過程中也需要對(duì)用戶的信息進(jìn)行脫敏處理活著加密處理,針對(duì)于比較繁雜的工作,個(gè)人來講解如何實(shí)現(xiàn)注解式脫敏,支持靜態(tài)調(diào)用和aop統(tǒng)一攔截實(shí)現(xiàn)脫敏或者加密返回。
代碼講解
脫敏枚舉類
定義枚舉類,處理所有脫敏和加密等,同時(shí)可擴(kuò)展性,這里只是注解式調(diào)用方法而已,以便編寫樣例。DesensitizationEnum若還需要其他脫敏或者加密方法是,只需要添加下面枚舉類型即可
package com.lgh.common.sensitive; import com.lgh.common.utils.MaskUtils; import java.lang.reflect.Method; /** * 若需要新定義一個(gè)掃描規(guī)則,這里添加即可 * * @author lgh * @version 1.0 * @date 2021/1/17 */ public enum DesensitizationEnum { // 執(zhí)行類和脫敏方法名 PHONE(MaskUtils.class, "maskPhone", new Class[]{String.class}); private Class<?> clazz; private Method method; DesensitizationEnum(Class<?> target, String method, Class[] paramTypes) { this.clazz = target; try { this.method = target.getDeclaredMethod(method, paramTypes); } catch (NoSuchMethodException e) { e.printStackTrace(); } } public Method getMethod() { return method; } }
脫敏工具
package com.lgh.common.utils; import org.springframework.util.StringUtils; /** * @author lgh * @version 1.0 * @date 2021/1/17 */ public class MaskUtils { public static String maskPhone(String phone){ if(StringUtils.isEmpty(phone) || phone.length() < 8){ return phone; } return phone.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"); } }
注解類編寫
此類添加到需要脫敏的類屬性上即可實(shí)現(xiàn)脫敏,具體是遞歸遍歷此注解,通過反射機(jī)制來實(shí)現(xiàn)脫敏功能
package com.lgh.common.sensitive; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 參數(shù)定義注解類 * @author linguohu * @version 1.0 * @date 2021/1/17 **/ @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SensitiveValid { DesensitizationEnum type(); }
脫敏工具類
特殊聲明,我們遞歸時(shí)是索引遞歸,會(huì)出現(xiàn)死循環(huán)的情況,比如對(duì)象引用了對(duì)象,循環(huán)地址引用,所以會(huì)出現(xiàn)死循環(huán),這里設(shè)置了10層遞歸,一般我們也不允許有那么深的對(duì)象設(shè)置。
package com.lgh.common.utils; import com.lgh.common.sensitive.DesensitizationEnum; import com.lgh.common.sensitive.SensitiveValid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.ReflectionUtils; import java.lang.reflect.Field; import java.util.Collection; import java.util.Map; /** * 對(duì)象脫敏工具 * * @author lgh * @version 1.0 * @date 2021/1/17 */ public class DesensitizationUtils { private static final Logger log = LoggerFactory.getLogger(DesensitizationUtils.class); private DesensitizationUtils() { } /** * 掃描對(duì)象注解,脫敏,最高層次8層 * * @param obj */ public static void format(Object obj) { DesensitizationUtils.formatMethod(obj, 10); } /** * 遞歸遍歷數(shù)據(jù),因?yàn)榭赡苡袑?duì)象地址應(yīng)用導(dǎo)致循環(huán)問題,同時(shí)設(shè)置莫名奇妙的異常,所以設(shè)置遞歸層次,一般不要超過10層 * * @param obj 需要反射對(duì)象 * @param level 遞歸層次,必須輸入 */ private static void formatMethod(Object obj, int level) { if (obj == null || isPrimitive(obj.getClass()) || level <= 0) { return; } if (obj.getClass().isArray()) { for (Object object : (Object[]) obj) { formatMethod(object, level--); } } else if (Collection.class.isAssignableFrom(obj.getClass())) { for (Object o : ((Collection) obj)) { formatMethod(o, level--); } } else if (Map.class.isAssignableFrom(obj.getClass())) { for (Object o : ((Map) obj).values()) { formatMethod(o, level--); } } else { objFormat(obj, level); } } /** * 只有對(duì)象才格式化數(shù)據(jù) * * @param obj * @param level */ private static void objFormat(Object obj, int level) { for (Field field : obj.getClass().getDeclaredFields()) { try { if (isPrimitive(field.getType())) { SensitiveValid sensitiveValid = field.getAnnotation(SensitiveValid.class); if (sensitiveValid != null) { ReflectionUtils.makeAccessible(field); DesensitizationEnum desensitizationEnum = sensitiveValid.type(); Object fieldV = desensitizationEnum.getMethod().invoke(null, field.get(obj)); ReflectionUtils.setField(field, obj, fieldV); } } else { ReflectionUtils.makeAccessible(field); Object fieldValue = ReflectionUtils.getField(field, obj); if (fieldValue == null) { continue; } formatMethod(fieldValue, level - 1); } } catch (Exception e) { log.error("脫敏數(shù)據(jù)處理異常", e); } } } /** * 基本數(shù)據(jù)類型和String類型判斷 * * @param clz * @return */ public static boolean isPrimitive(Class<?> clz) { try { if (String.class.isAssignableFrom(clz) || clz.isPrimitive()) { return true; } else { return ((Class) clz.getField("TYPE").get(null)).isPrimitive(); } } catch (Exception e) { return false; } } }
脫敏AOP的實(shí)現(xiàn)
aop插拔式編程,以便防止有不需要的操作,所以編寫可控制類注解EnableDesensitization
package com.lgh.common.sensitive; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 方法返回值攔截器,需要注解才生效 * @author lgh * @version 1.0 * @date 2021/1/17 **/ @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface EnableDesensitization { }
最后實(shí)現(xiàn)攔截aop
package com.lgh.common.sensitive; import com.lgh.common.utils.DesensitizationUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.context.annotation.Configuration; import org.aspectj.lang.annotation.Aspect; import java.lang.reflect.Method; /** * @author lgh * @version 1.0 * @date 2021/1/17 */ @Aspect @Configuration public class SensitiveAspect { public static final String ACCESS_EXECUTION = "execution(* com.lgh.controller..*.*(..))"; /** * 注解脫敏處理 * * @param joinPoint * @return * @throws Throwable */ @Around(ACCESS_EXECUTION) public Object sensitiveClass(ProceedingJoinPoint joinPoint) throws Throwable { return sensitiveFormat(joinPoint); } /** * 插拔式注解統(tǒng)一攔截器。@{link EnableDesensitization } 和 @SensitiveValid * * @param joinPoint * @return * @throws Throwable */ public Object sensitiveFormat(ProceedingJoinPoint joinPoint) throws Throwable { Object obj = joinPoint.proceed(); if (obj == null || DesensitizationUtils.isPrimitive(obj.getClass())) { return obj; } MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); EnableDesensitization desensitization = joinPoint.getTarget().getClass().getAnnotation(EnableDesensitization.class); if (desensitization != null || method.getAnnotation(EnableDesensitization.class) != null) { DesensitizationUtils.format(obj); } return obj; } }
實(shí)戰(zhàn)演練
我居于上一章節(jié)的UserDetail對(duì)象增加phone字段,同時(shí)加入注解,如下:
package com.lgh.common.authority.entity; import com.lgh.common.sensitive.DesensitizationEnum; import com.lgh.common.sensitive.SensitiveValid; public class UserDetail { private long id; private String name; @SensitiveValid(type = DesensitizationEnum.PHONE) private String phone; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public void setPhone(String phone) { this.phone = phone; } public String getPhone() { return phone; } }
接下來controller中啟動(dòng)注解
@GetMapping("/detail") @EnableDesensitization public IResult<UserDetail> getUser(@AuthenticationPrincipal UserDetail userDetail) { return CommonResult.successData(userDetail); }
大功告成,接下來我們實(shí)現(xiàn)一下訪問操作
github,以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
sms4j?2.0?全新來襲功能的調(diào)整及maven變化詳解
這篇文章主要介紹了sms4j?2.0?全新來襲功能的調(diào)整及maven變化詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04詳解mysql插入數(shù)據(jù)后返回自增ID的七種方法
這篇文章主要介紹了詳解mysql插入數(shù)據(jù)后返回自增ID的七種方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12springmvc項(xiàng)目使用@Valid+BindingResult遇到的問題
這篇文章主要介紹了springmvc項(xiàng)目使用@Valid+BindingResult遇到的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12使用Java WebSocket獲取客戶端IP地址的示例代碼
在開發(fā)Web應(yīng)用程序時(shí),我們通常需要獲取客戶端的 IP 地址用于日志記錄、身份驗(yàn)證、限制訪問等操作,本文將介紹如何使用Java WebSocket API獲取客戶端IP地址,以及如何在常見的WebSocket框架中獲得客戶端 IP地址,需要的朋友可以參考下2023-11-11SpringBoot集成Redisson實(shí)現(xiàn)延遲隊(duì)列的場景分析
這篇文章主要介紹了SpringBoot集成Redisson實(shí)現(xiàn)延遲隊(duì)列,本文通過場景分析實(shí)例代碼相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04