⾃定义Json注解实现输出⽇志字段脱敏
背景
在⽇志输出的时候,有时会输出⼀些⽤户的敏感信息,如⼿机号,⾝份证号,银⾏卡号等,现需要对这些信息在⽇志输出的时候进⾏脱敏处理
思路
使⽤fastjson的ValueFilter对带有⾃定义注解的字段进⾏过滤
/**
* 敏感信息类型 *
* @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-对象 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对功能没有影响,空处理 }
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注解式脱敏
随着互联⽹时代普及,⽤户的信息越来越重要,我们开发软件过程中也需要对⽤户的信息进⾏脱敏处理活着加密处理,针对于⽐较繁杂的⼯作,个⼈来讲解如何实现注解式脱敏,⽀持静态调⽤和aop统⼀拦截实现脱敏或者加密返回。
代码讲解
脱敏枚举类
定义枚举类,处理所有脱敏和加密等,同时可扩展性,这⾥只是注解式调⽤⽅法⽽已,以便编写样例。DesensitizationEnum若还需要其他脱敏或者加密⽅法是,只需要添加下⾯枚举类型即可
package com.lgh.common.sensitive;import com.lgh.common.utils.MaskUtils;import java.lang.reflect.Method;/**
* 若需要新定义⼀个扫描规则,这⾥添加即可 *
* @author lgh * @version 1.0 * @date 2021/1/17 */
public enum DesensitizationEnum { // 执⾏类和脱敏⽅法名
PHONE(MaskUtils.class, \"maskPhone\ 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})\ }}
注解类编写
此类添加到需要脱敏的类属性上即可实现脱敏,具体是递归遍历此注解,通过反射机制来实现脱敏功能
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 linguohu * @version 1.0 * @date 2021/1/17 **/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)@Documented
public @interface SensitiveValid { DesensitizationEnum type();}
脱敏⼯具类
特殊声明,我们递归时是索引递归,会出现死循环的情况,⽐如对象引⽤了对象,循环地址引⽤,所以会出现死循环,这⾥设置了10层递归,⼀般我们也不允许有那么深的对象设置。
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;/**
* 对象脱敏⼯具 *
* @author lgh * @version 1.0 * @date 2021/1/17 */
public class DesensitizationUtils {
private static final Logger log = LoggerFactory.getLogger(DesensitizationUtils.class); private DesensitizationUtils() { } /**
* 扫描对象注解,脱敏,最⾼层次8层 *
* @param obj */
public static void format(Object obj) {
DesensitizationUtils.formatMethod(obj, 10); } /**
* 递归遍历数据,因为可能有对象地址应⽤导致循环问题,同时设置莫名奇妙的异常,所以设置递归层次,⼀般不要超过10层 *
* @param obj 需要反射对象
* @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); } } /**
* 只有对象才格式化数据 *
* @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(\"脱敏数据处理异常\ } } } /**
* 基本数据类型和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的实现
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 {}
最后实现拦截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); } /**
* 插拔式注解统⼀拦截器。@{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; }}
实战演练
我居于上⼀章节的UserDetail对象增加phone字段,同时加⼊注解,如下:
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中启动注解
@GetMapping(\"/detail\") @EnableDesensitization
public IResult ⼤功告成,接下来我们实现⼀下访问操作 ,以上为个⼈经验,希望能给⼤家⼀个参考,也希望⼤家多多⽀持。 因篇幅问题不能全部显示,请点此查看更多更全内容