数据脱敏-Jackson-Fastjson-Logback

Posted by Yezhiwei on July 7, 2020

原文地址:https://blog.csdn.net/qq_26418435/article/details/103620548

学习思路

  • 分析脱敏场景
  • 基于 Fastjson、Jackson、Logback 的各种实现
  • 总结
  • 文末有代码实现 git 地址、小星星

一、分析脱敏场景

生产数据,为了保护用户信息,防止用户信息泄露,我们通常需要对数据进行脱敏主要有(手机号、身份证、姓名等)

  1. 打印日志脱敏,日志中看到的信息不是完整的比如:183****0001
  2. 接口返回信息脱敏,比如用户手机号、银行卡号、身份证等
  3. 数据库脱敏存储

题外话:数据库存储目前用的比较多的是密码加密,其它的数据牵扯到查询,加密成本比较高,sharding-jdbc 自带数据加密存储及查询,有兴趣的可以了解一下,我们本节主要讲解前两种

二、基于 Fastjson、Jackson、Logback 的各种实现

1、Fastjson 实现

  • 实现思路:
  1. 自定义注解,可让用户自定义脱敏方式,用于实体类的属性
  2. 基于 ValueFilter 进行属性注解拦截,并对 value 进行替换脱敏
  3. 使用 json 序列化对象是指定自定义序列化 Filter

题外话:ValueFilter:对象值过滤器,将要序列化对象的值进行统一处理

  • 代码实现:

首先自定义注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Desensitization {​
    /**
     * 脱敏规则类型
     * @return
     */
    DesensitionType type();​
    /**
     * 附加值, 自定义正则表达式等
     * @return
     */
    String[] attach() default "";
​}

解释:

  1. 脱敏规则类型:主要定义了一些常用的类型(手机号、身份证等)
  2. 自定义正则表达式,如果常用的不能满足时可自定义

看一下脱敏规则类型

public enum DesensitionType { 

    /**
     * 手机号脱敏
     * "(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}"
     * "(\\d{3})\\d{4}(\\d{4})"
     */
    PHONE("mobile", "11位手机号", "(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{4}(\\d{4}}", "$1****$2"), 
    IDENTITYNO("identityNo", "15或者18身份证号", "(\\w{4})\\w{7,10}(\\w{4})", "$1****$2"), 
    BANKCARDNO("bankCardNo", "银行卡号", "(\\d{4})\\d*(\\d{4})", "$1****$2"), 
    REALNAME("realname","真实姓名Json类型","(\"realname\":)(\"[\u4E00-\u9FA5]{1})[\u4E00-\u9FA5]{1,}(\")","$1$2**$3"), 
    REALNAME2("realname","真实姓名toString类型","(realname=)([\u4E00-\u9FA5]{1})[\u4E00-\u9FA5]{1,}","$1$2**"), 
    CUSTOM("custom", "自定义正则处理", ""), 
    TRUNCATE("truncate", "字符串截取处理", ""), 
    ;  

    String type; 
    String describe; 
    String[] regular; 

    DesensitionType(String type, String describe, String... regular) { 
        this.type = type; 
        this.describe = describe; 
        this.regular = regular; 
    }
}

这里主要注意一下第三个参数:是一个数组,通常 0 位:要脱敏数据的正则匹配,1 位:要脱敏成的格式

然后在我们的需要脱敏的对象字段上加上该注解

@Data
public class UserDTO implements Serializable {
    @Desensitization(type = DesensitionType.IDENTITYNO)
    private String identityNo;
    private String name;
    private String realname;
}

接下来编写我们的自定义值过滤器,实现 ValueFilter,实现方法 process()

@Log4j2
public class FastjsonDesensitizeFilter implements ValueFilter,DesensitizeService { 

@Override 
public Object process(Object object, String name, Object value) {
    if (null == value || !(value instanceof String) || ((String) value).length() == 0) {
        return value;
    }

    try {
        Field field = object.getClass().getDeclaredField(name);
        Desensitization desensitization;
            
        if (String.class != field.getType() || (desensitization = field.getAnnotation(Desensitization.class)) == null) {
            return value;
        }
        DesensitionType type = desensitization.type();
        List<String> regular = this.desensitize(type, desensitization);
    
        if (regular.size() > 1) {
            String match = regular.get(0);
            String result = regular.get(1);
            if (null != match && result != null && match.length() > 0) {
                return ((String) value).replaceAll(match, result);
            }
        }
    } catch (Exception e) {
        log.warn("FastJsonDesensitizeFilter the class {} has no field {}", object.getClass(), name);
    }
    return value;
}

解释:

  1. 这里目前只支持 String 类型的 value,大家可以根据需要自定义
  2. 获取属性上的注解,根据属性得到相应的脱敏规则类型
  3. 按照规则类型进行 value 替换

然后只要我们在使用 fastjson 进行序列化的时候指定我们的自定义过滤器即可

public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { 
    log.info("requestUrl 【{}】 request body 【{}】", request.getURI(), JSONObject.toJSONString(body, new FastjsonDesensitizeFilter())); 
    return body;
}

打印出来就是这样

requestUrl 【http://localhost:8080/idNo】 request body 【{"code":"000000","data":{"identityNo":"1111****1111","name":"dsf","realname":"张**"},"message":"SUCCESS"}】

2、基于 Jackson 实现数据脱敏

思路跟用 Fastjson 基本一样,只是实现的类不同而已

public class JacksonDesensitize extends JsonSerializer<String> implements ContextualSerializer, DesensitizeService {

    private DesensitionType type;

    @Override
    public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
    
        if (type != null){
            try {
                List<String> regular = this.desensitize(type, null);
                if (regular.size() > 1) {
                    String match = regular.get(0);
                    String result = regular.get(1);
                    if (null != match && result != null && match.length() > 0) {
                        jsonGenerator.writeString(value.replaceAll(match, result));
                    }
                }
            } catch (Exception e) {
                log.warn("JacksonDesensitize has no field {}",  value);
            }
        }
    }​

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
        type = beanProperty.getAnnotation(Desensitization.class).type();
        return this;
    }
}

解释:

  1. 获取对象属性上的注解,根据属性得到相应的脱敏规则类型
  2. 按照规则类型进行 value 替换

题外话:这里主要牵扯到要继承 JsonSerializer 重写 serialize() 方法实现对象序列化,实现 ContextualSerializer 接口,实现方法,这个主要是拿到属性上的注解

接下来引用我们的自定义序列化器即可,直接在自定义注解上引用即可

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = JacksonDesensitize.class)
@JacksonAnnotationsInside
public @interface Desensitization {
​
    /**
     * 脱敏规则类型
     * @return
     */
    DesensitionType type();
​
    /**
     * 附加值, 自定义正则表达式等
     * @return
     */
    String[] attach() default "";
​
}

解释:

  1. 这里主要看 @JsonSerialize(using = JacksonDesensitize.class) 就是让 json 序列化使用我们自定义的
  2. @JacksonAnnotationsInside 这个是注解组合出现时用的

我们现在的项目基本都是前后端分离,Controller 返回的时候一般都是 ResponseBody 的,正好我们 SpringMVC 后台是默认使用 Jackson 作为序列化的,所以这时候就可以直接使用

返回就是这样的

{
    "code": "000000",
    "message": "SUCCESS",
    "data": {
        "identityNo": "1111****1111",
        "name": "dsf",
        "realname": "张三"
    }
}

3、基于 Logback 进行全局日志脱敏

思路

  1. 我们先定义需要脱敏的属性名,就是你真正要打印到日志的属性名字
  2. 然后继承 Logback 的 MessageConverter 重写 convert 方法
  3. 通过正则进行身份证、姓名、手机号的匹配
  4. 匹配成功后按规则替换
public class LogDesensitizeConverter extends MessageConverter {

    /**
     * 日志脱敏开关
     */
    private static Boolean converterCanRun = Boolean.TRUE;

    /**
     * 日志脱敏关键字
     */
    @Override
    public String convert(ILoggingEvent event) {
        // 获取原始日志
        String oriLogMsg = event.getFormattedMessage();

        if (!converterCanRun){
            return oriLogMsg;
        }
        // 获取脱敏后的日志
        DesensitionType[] values = DesensitionType.values();
        for (DesensitionType value : values) {
            if (value.getRegular() != null && value.getRegular().length > 0 && oriLogMsg.contains(value.getType())) {
                Matcher matcher = Pattern.compile(value.getRegular()[0]).matcher(oriLogMsg);
                oriLogMsg = matcher.replaceAll(value.getRegular()[1]);
            }
        }
        return oriLogMsg;
    }
}

然后将我们编写完的 Converter 添加到 logback.xml 文件引用即可

之后接口用了,日志文件里是这样的

三、总结

  1. SpringMVC 默认使用 Jackson 作为对象序列化,如果想要使用 Fastjson 需要单独配置,然后指定我们的自定义序列化器就可以了
  2. 如果单纯的只用 Fastjson 打印日志那么建议在拦截器,或者像本文代码中实现 ResponseBodyAdvice 去集中打印,不然还要每次都加我们的自定义序列化器
  3. 我们使用 Jackson 和 Fastjson 时使用了自定义注解,当然也可以根据自己的业务提前定义属性值,像 Logback 的方式一样实现也可
  4. 复杂的正则很耗 cpu 但我们的非常简单,并且都有提前过滤

四、代码

git 地址:https://gitee.com/carpentor/spring-cloud-example.git

如果觉得还有帮助的话,你的关注和转发是对我最大的支持,O(∩_∩)O: