【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);
}
}
}
}

浙公网安备 33010602011771号