【Java】Springboot 响应外切 实现数据脱敏
实现效果:
1、脱敏注解在模型类进行标记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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、测试接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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,相比切面可以控制的入口粒度到一个类上
标记注解,可以设置在类或者方法上:
1 2 3 4 5 6 7 8 9 10 11 12 | package cn.cloud9.server.struct.masking.annotation; import java.lang.annotation.*; /** * 标记Controller是否开启脱敏处理 */ @Documented @Retention (RetentionPolicy.RUNTIME) @Target ({ElementType.METHOD, ElementType.TYPE}) public @interface ActiveDataMasking { } |
和之前字典翻译类似,脱敏的具体字段注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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的脱敏工具类的方法,如果不符合实际开发要求,可以自己编写脱敏工具类进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | 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方法声明,就可以更直接点。。。
默认注解声明的自定义类如下,该方法不做任何处理:
1 2 3 4 5 6 7 8 9 10 | package cn.cloud9.server.struct.masking.intf; /** * 自定义脱敏实现 */ public class CustomMasking { public String masking(String data) { return data; } } |
如果需要具体脱敏,可以继承自定义类重写脱敏方法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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钩子之前,都响应出去了,才脱敏就没用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | 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); } } } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理