使用自定义Json注解实现输出日志字段脱敏

 更新时间:2021年12月18日 10:13:53   作者:worstezreal  
这篇文章主要介绍了使用自定义Json注解实现输出日志字段脱敏,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

自定义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", 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");
    }
}

注解类编写

此类添加到需要脱敏的类属性上即可实现脱敏,具体是递归遍历此注解,通过反射机制来实现脱敏功能

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("脱敏数据处理异常", e);
            }
        }
    }
    /**
     * 基本数据类型和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<UserDetail> getUser(@AuthenticationPrincipal UserDetail userDetail) {
        return CommonResult.successData(userDetail);
    }

大功告成,接下来我们实现一下访问操作

github,以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • sms4j 2.0 全新来袭功能的调整及maven变化详解

    sms4j 2.0 全新来袭功能的调整及maven变化详解

    这篇文章主要介绍了sms4j 2.0 全新来袭功能的调整及maven变化详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • 详解mysql插入数据后返回自增ID的七种方法

    详解mysql插入数据后返回自增ID的七种方法

    这篇文章主要介绍了详解mysql插入数据后返回自增ID的七种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • 详解Java 中程序内存的分析

    详解Java 中程序内存的分析

    这篇文章主要介绍了详解Java 中程序内存的分析的相关资料,需要的朋友可以参考下
    2017-04-04
  • java中DES加密解密

    java中DES加密解密

    本文给大家分享的是一段java中实现des加密解密的代码,非常的实用,基本每个项目都可以用到,推荐给大家。
    2015-03-03
  • Java 变量类型及其实例

    Java 变量类型及其实例

    这篇文章主要讲解Java中变量的类型以及实例,希望能给大家做一个参考
    2017-04-04
  • Java编程中ArrayList源码分析

    Java编程中ArrayList源码分析

    这篇文章主要介绍了Java编程中ArrayList源码分析,具有一定借鉴价值,需要的朋友可以参考下。
    2017-12-12
  • Java编程BigDecimal用法实例分享

    Java编程BigDecimal用法实例分享

    这篇文章主要介绍了Java编程BigDecimal用法实例分享,具有一定参考价值,需要的朋友可以了解下。
    2017-11-11
  • springmvc项目使用@Valid+BindingResult遇到的问题

    springmvc项目使用@Valid+BindingResult遇到的问题

    这篇文章主要介绍了springmvc项目使用@Valid+BindingResult遇到的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • 使用Java WebSocket获取客户端IP地址的示例代码

    使用Java WebSocket获取客户端IP地址的示例代码

    在开发Web应用程序时,我们通常需要获取客户端的 IP 地址用于日志记录、身份验证、限制访问等操作,本文将介绍如何使用Java WebSocket API获取客户端IP地址,以及如何在常见的WebSocket框架中获得客户端 IP地址,需要的朋友可以参考下
    2023-11-11
  • SpringBoot集成Redisson实现延迟队列的场景分析

    SpringBoot集成Redisson实现延迟队列的场景分析

    这篇文章主要介绍了SpringBoot集成Redisson实现延迟队列,本文通过场景分析实例代码相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-04-04

最新评论