【Java】Springboot 响应外切 实现数据脱敏

 

实现效果:

1、脱敏注解在模型类进行标记

package cn.cloud9.server.test.model;

import cn.cloud9.server.struct.masking.annotation.MaskingField;
import cn.cloud9.server.struct.masking.enums.MaskingType;
import cn.cloud9.server.struct.masking.intf.impl.TestCustomMasking;
import lombok.Data;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月27日 上午 11:28
 */
@Data
public class MaskingModel {

    private String idCard;

    @MaskingField(srcField = "idCard", maskingType = MaskingType.ID_CARD)
    private String maskedIdCard;

    @MaskingField(srcField = "idCard", usingCustom = true, custom = TestCustomMasking.class)
    private String customMaskedIdCard;
}

  

2、测试接口:

package cn.cloud9.server.test.controller;

import cn.cloud9.server.struct.masking.annotation.ActiveDataMasking;
import cn.cloud9.server.test.model.MaskingModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月27日 上午 11:35
 */
@ActiveDataMasking
@RestController
@RequestMapping("/test/data-mask")
public class MaskingController {

    @GetMapping("/demo")
    public MaskingModel getMaskingData() {
        final MaskingModel maskingModel = new MaskingModel();
        maskingModel.setIdCard("362202198708064434");
        return maskingModel;
    }
}

  

3、脱敏结果:

 

 

代码实现:

使用ResponseBodyAdvice,相比切面可以控制的入口粒度到一个类上

 

标记注解,可以设置在类或者方法上:

package cn.cloud9.server.struct.masking.annotation;

import java.lang.annotation.*;

/**
 * 标记Controller是否开启脱敏处理
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface ActiveDataMasking {
}

  

和之前字典翻译类似,脱敏的具体字段注解

package cn.cloud9.server.struct.masking.annotation;

import cn.cloud9.server.struct.masking.enums.MaskingType;
import cn.cloud9.server.struct.masking.intf.CustomMasking;

import java.lang.annotation.*;

/**
 * 标记需要脱敏的字段,
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface MaskingField {
    /* 声明脱敏的字段来源 */
    String srcField();

    /* 声明脱敏的类型, 默认无类型 */
    MaskingType maskingType() default MaskingType.NONE;

    /* 自定义脱敏类 */
    Class<? extends CustomMasking> custom() default CustomMasking.class;

    /* 是否使用自定义 */
    boolean usingCustom() default false;

}

  

脱敏类型被标记为枚举,每种类型设定对应的脱敏方法:

这里我直接采用Hutool的脱敏工具类的方法,如果不符合实际开发要求,可以自己编写脱敏工具类进行处理

package cn.cloud9.server.struct.masking.enums;

import cn.hutool.core.util.DesensitizedUtil;
import lombok.Getter;

import java.util.function.Function;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月26日 下午 11:24
 */
@Getter
public enum MaskingType {
    NONE("不脱敏", data -> data),
    ID_CARD("身份证", MaskingType::maskingIdCard),
    PHONE("手机号", DesensitizedUtil::mobilePhone),
    NAME("姓名", DesensitizedUtil::chineseName),
    ADDRESS("地址", MaskingType::maskingAddress),
    EMAIL("邮箱", DesensitizedUtil::email),
    LICENSE_PLATE("车牌号", DesensitizedUtil::carLicense),
    PASSWORD("密码", DesensitizedUtil::password),
    BANKCARD("银行卡", DesensitizedUtil::bankCard),
   ;

    private final String define;
    private final Function<String, String> maskingFunc;

    MaskingType(String define, Function<String, String> maskingFunc) {
        this.define = define;
        this.maskingFunc = maskingFunc;
    }

    public static String maskingIdCard(String idCardNo) {
        return DesensitizedUtil.idCardNum(idCardNo, 4, 4);
    }

    public static String maskingAddress(String address) {
        return DesensitizedUtil.address(address, 8);
    }
}

  

上述枚举的类型是泛用的情况,如果业务上还需要特殊字段脱敏需求,考虑到这点

在注解上增加是否使用自定义脱敏,如果是,则声明自定义脱敏类,如果枚举可以支持Lambda方法声明,就可以更直接点。。。

默认注解声明的自定义类如下,该方法不做任何处理:

package cn.cloud9.server.struct.masking.intf;

/**
 * 自定义脱敏实现
 */
public class CustomMasking {
    public String masking(String data) {
        return data;
    }
}

  

如果需要具体脱敏,可以继承自定义类重写脱敏方法实现:

package cn.cloud9.server.struct.masking.intf.impl;

import cn.cloud9.server.struct.masking.intf.CustomMasking;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月27日 下午 01:07
 */
public class TestCustomMasking extends CustomMasking {

    @Override
    public String masking(String data) {
        return "具体脱敏实现。。。";
    }
}

  

钩子编写,就使用ResponseBodyAdvice完成,实现和字典翻译大同小异:

注意执行顺序,要在响应Advice钩子之前,都响应出去了,才脱敏就没用了

package cn.cloud9.server.struct.masking.hook;

import cn.cloud9.server.struct.masking.annotation.ActiveDataMasking;
import cn.cloud9.server.struct.masking.annotation.MaskingField;
import cn.cloud9.server.struct.masking.enums.MaskingType;
import cn.cloud9.server.struct.masking.intf.CustomMasking;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.SneakyThrows;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月26日 下午 11:16
 */
@Order(97)
@ControllerAdvice(annotations = RestController.class)
public class DataMaskingHook implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        final Class<ActiveDataMasking> admClass = ActiveDataMasking.class;
        boolean isMarkOnClass = Objects.nonNull(methodParameter.getContainingClass().getAnnotation(admClass));
        boolean isMarkOnMethod = Objects.nonNull(methodParameter.getMethodAnnotation(admClass));
        return isMarkOnClass || isMarkOnMethod;
    }

    @SuppressWarnings("all")
    @Override
    public Object beforeBodyWrite(
            Object body,
            MethodParameter methodParameter,
            MediaType mediaType,
            Class<? extends HttpMessageConverter<?>> aClass,
            ServerHttpRequest serverHttpRequest,
            ServerHttpResponse serverHttpResponse
    ) {
        /* 是否为空 */
        final boolean isEmpty = Objects.isNull(body);
        if (isEmpty) return body;
        /* 返回的结果类型是否为基本类型 */
        final boolean isPrimitive = body.getClass().isPrimitive();
        if (isPrimitive) return body;

        /* 返回的结果类型是否为集合 */
        final boolean isCollection = body instanceof Collection;
        /* 返回的结果类型是否为翻页对象 */
        final boolean isPage = body instanceof IPage;

        if (isCollection) {
            Collection<Object> list = (Collection<Object>) body;
            if (CollectionUtils.isEmpty(list)) return body;
            for (Object row : list) masking(row);
        } else if (isPage) {
            IPage<Object> page = (IPage<Object>) body;
            if (CollectionUtils.isEmpty(page.getRecords())) return body;
            final List<Object> records = page.getRecords();
            for (Object record : records) masking(record);
        } else {
            masking(body);
        }

        return body;
    }

    @SneakyThrows
    private void masking(Object body) {
        /* 获取这个类下的所有字段 */
        final Field[] declaredFields = body.getClass().getDeclaredFields();
        for (Field field : declaredFields) {
            /* 处理嵌套在目标对象类中的集合类型脱敏 */
            final Object fieldValue = BeanUtil.getFieldValue(body, field.getName());
            final boolean isCollection = fieldValue instanceof Collection;
            final boolean isPage = fieldValue instanceof IPage;
            if (isCollection) {
                Collection<Object> list = (Collection<Object>) fieldValue;
                if (CollectionUtils.isEmpty(list)) continue;
                for (Object row : list) {
                    if (row.getClass().isPrimitive()) continue;
                    this.masking(row);
                }
            } else if (isPage) {
                IPage<Object> page = (IPage<Object>) fieldValue;
                if (CollectionUtils.isEmpty(page.getRecords())) continue;
                final List<Object> records = page.getRecords();
                for (Object record : records) {
                    if (record.getClass().isPrimitive()) continue;
                    this.masking(record);
                }
            }

            /* 获取类上的@MaskingField注解 */
            final MaskingField mf = field.getAnnotation(MaskingField.class);
            /* 如果没有此注解则跳过 */
            if (Objects.isNull(mf)) continue;

            /* 获取注解声明的源字段,和对应的脱敏类型 */
            final String srcField = mf.srcField();
            final MaskingType maskingType = mf.maskingType();
            final boolean usingCustom = mf.usingCustom();

            /* 没有声明脱敏类型,也不使用自定义脱敏接口实现,则跳过,不执行 */
            if (MaskingType.NONE.equals(maskingType) && !usingCustom) continue;

            /* 取出目标对象对应字段的值 */
            final Object bodyFieldVal = BeanUtil.getFieldValue(body, srcField);
            /* 如果为空,类型不是String,不处理 */
            if (Objects.isNull(bodyFieldVal) || !(bodyFieldVal instanceof String)) continue;

            final String data = String.valueOf(bodyFieldVal);
            /* 使用自定义脱敏接口 */
            if (usingCustom) {
                /* 获取自定义脱敏对象, 执行脱敏操作 */
                final CustomMasking customMasking = mf.custom().newInstance();
                final String masking = customMasking.masking(data);
                /* 给当前字段赋值 */
                BeanUtil.setFieldValue(body, field.getName(), masking);
            } else {
                /* 使用枚举声明的脱敏方法来处理 */
                final Function<String, String> maskingFunc = maskingType.getMaskingFunc();
                final String masking = maskingFunc.apply(data);
                /* 给当前字段赋值 */
                BeanUtil.setFieldValue(body, field.getName(), masking);
            }
        }
    }
}

  

 

posted @ 2022-11-27 13:40  emdzz  阅读(1370)  评论(0编辑  收藏  举报