使用自定义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);
    }

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Java利用Jackson序列化实现数据脱敏

    几天前使用了Jackson对数据的自定义序列化.突发灵感,利用此方法来简单实现接口返回数据脱敏,故写此文记录. 核心思想是利用Jackson的StdSerializer,@JsonSerialize,以及自己实现的数据脱敏过程. 使用效果如下: 首先在需要进行脱敏的VO字段上面标注相关脱敏注解 调用接口即可看到脱敏效果 实现过程如下: 1. 定义脱敏的过程实现 /** * Created by EalenXie on 2021/9/24 15:52 * 顶级的脱敏器 */ public inte

  • Springboot敏感字段脱敏的实现思路

    生产环境用户的隐私数据,比如手机号.身份证或者一些账号配置等信息,入库时都要进行不落地脱敏,也就是在进入我们系统时就要实时的脱敏处理. 用户数据进入系统,脱敏处理后持久化到数据库,用户查询数据时还要进行反向解密.这种场景一般需要全局处理,那么用AOP切面来实现在适合不过了. 首先自定义两个注解@EncryptField.@EncryptMethod分别用在字段属性和方法上,实现思路很简单,只要方法上应用到@EncryptMethod注解,则检查入参字段是否标注@EncryptField注解,有则

  • 关于fastjson的@JSONField注解的一些问题(详解)

    @JSONField 看源码它可以作用于字段和方法上. 引用网上说的, 一.作用Field @JSONField作用在Field时,其name不仅定义了输入key的名称,同时也定义了输出的名称. 但是我在使用中,发现并不如上所说. 例如 @JSONField(name="project_id") private Long ProjectID 发现bean 转json的时候并是"project_id":xxx的形式,json转bean的时候也不会把"proj

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

    自定义Json注解实现输出日志字段脱敏 背景 在日志输出的时候,有时会输出一些用户的敏感信息,如手机号,身份证号,银行卡号等,现需要对这些信息在日志输出的时候进行脱敏处理 思路 使用fastjson的ValueFilter对带有自定义注解的字段进行过滤 /** * 敏感信息类型 * * @author worstEzreal * @version V1.0.0 * @date 2017/7/19 */ public enum SensitiveType { ID_CARD, BANK_CARD,

  • SpringBoot 自定义注解实现涉密字段脱敏

    目录 1. 创建隐私数据类型枚举:PrivacyTypeEnum 2. 创建自定义隐私注解:PrivacyEncrypt 3. 创建自定义序列化器:PrivacySerializer 4. 隐私数据隐藏工具类:PrivacyUtil 5. 注解使用 关于数据脱敏,网上的文章都是硬编码规则,比如对身份证,手机号,邮件地址等固定写法脱敏.本文在此基础上,拓展动态从数据库查出涉密关键字执行脱敏操作. 数据脱敏:把系统里的一些敏感数据进行加密处理后再返回,达到保护隐私作用,实现效果图如下: 其实要实现上

  • logback自定义json日志输出示例详解

    目录 前言 依赖的jar maven坐标 配置Appender节点 appender配置说明: 配置logger节点 logger配置说明: 前言 先说下楼主的使用场景吧,将程序的某些方法调用以json格式的内容记录到文件中,提供给大数据做数据分析用.当然这个需求实现起来很简单,通过aop拦截切面统一输出内容到文件即可.下面要介绍的就是通过logback日志体系以及logstash提供的json log依赖将数据以json格式记录到日志文件的例子. 依赖的jar logstash-logback

  • spring AOP自定义注解方式实现日志管理的实例讲解

    今天继续实现AOP,到这里我个人认为是最灵活,可扩展的方式了,就拿日志管理来说,用Spring AOP 自定义注解形式实现日志管理.废话不多说,直接开始!!! 关于配置我还是的再说一遍. 在applicationContext-mvc.xml中要添加的 <mvc:annotation-driven /> <!-- 激活组件扫描功能,在包com.gcx及其子包下面自动扫描通过注解配置的组件 --> <context:component-scan base-package=&qu

  • Laravel5.5+ 使用API Resources快速输出自定义JSON方法详解

    从Laravel 5.5+开始,加入了API Resources这个概念. 我们先来看一下官网如何定义这个概念的: When building an API, you may need a transformation layer that sits between your Eloquent models and the JSON responses that are actually returned to your application's users. Laravel's resour

  • Java 如何通过注解实现接口输出时数据脱敏

    目录 Java注解实现接口输出数据脱敏 先声明了一个注解 我们目前只支持对手机号 然后我们需要实现注解的拦截功能 我对默认声明和脱敏名称和手机号进行了测试 Java注解的字段脱敏处理 定义需要脱敏的字段规则 声明注解 测试 Java注解实现接口输出数据脱敏 在后台管理中,对于手机号,身份证,姓名这些数据不允许所有人都能看,这时候我们要对相对数据进行脱敏. 先声明了一个注解 通过对相关接口函数进行声明,以及配置需要脱敏的参数类型SecretTypeEnum,默认脱敏手机号 /** * 脱敏声明 *

  • golang如何自定义json序列化应用详解

    前言 Go语言作为一个由Google开发,号称互联网的C语言的语言,自然也对JSON格式支持很好.下面这篇文章主要介绍了关于golang自定义json序列化应用的相关内容,下面话不多说了,来一起看看详细的介绍吧 问题引入 当某个struct存在某个字段为string或者[]byte类型但是实际上保存的内容是json格式的数据时,对其进行json序列化,比如 type Message struct { From string `json:"from"` To string `json:&

  • Python3自定义json逐层解析器代码

    用python3对json内容逐层进行解析,拿中国天气网的接口返回数据测试, 代码如下: # -*- coding: utf-8 -*- import operator as op from collections import defaultdict class Json(object): def __init__(self, json: str): sth = eval(json) load = lambda sth: sth if op.eq(type(sth).__name__, dic

  • logrus hook输出日志到本地磁盘的操作

    logrus是go的一个日志框架,它最让人激动的应该是hook机制,可以在初始化时为logrus添加hook,logrus可以实现各种扩展功能,可以将日志输出到elasticsearch和activemq等中间件去,甚至可以输出到你的email和叮叮中去,不要问为为什么可以发现可以输入到叮叮中去,都是泪,手动笑哭! 言归正传,这里就简单的通过hook机制将文件输出到本地磁盘. 首先 go get github.com/sirupsen/logrus 然后 logrus和go lib里面一样有6个

  • 浅谈自定义校验注解ConstraintValidator

    目录 一.前言 二.自定义参数校验器 三.使用自定义注解 一.前言 系统执行业务逻辑之前,会对输入数据进行校验,检测数据是否有效合法的.所以我们可能会写大量的if else等判断逻辑,特别是在不同方法出现相同的数据时,校验的逻辑代码会反复出现,导致代码冗余,阅读性和可维护性极差. JSR-303是Java为Bean数据合法性校验提供的标准框架,它定义了一整套校验注解,可以标注在成员变量,属性方法等之上. hibernate-validator就提供了这套标准的实现,我们在用Springboot开

随机推荐