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