实现效果:

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

  

【Java】Springboot 响应外切 实现数据脱敏的更多相关文章

  1. Springboot 配置文件、隐私数据脱敏的最佳实践(原理+源码)

    大家好!我是小富- 这几天公司在排查内部数据账号泄漏,原因是发现某些实习生小可爱居然连带着账号.密码将源码私传到GitHub上,导致核心数据外漏,孩子还是没挨过社会毒打,这种事的后果可大可小. 说起这 ...

  2. java 数据脱敏

    所谓数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护.在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并提供测试使用,如身份 ...

  3. 如何用java实现数据脱敏

    数据脱敏是什么意思呢? 数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护.在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并 ...

  4. SpringBoot进阶教程(七十五)数据脱敏

    无论对于什么业务来说,用户数据信息的安全性无疑都是非常重要的.尤其是在数字经济大火背景下,数据的安全性就显得更加重要.数据脱敏可以分为两个部分,一个是DB层面,防止DB数据泄露,暴露用户信息:一个是接 ...

  5. java springboot activemq 邮件短信微服务,解决国际化服务的国内外兼容性问题,含各服务商调研情况

    java springboot activemq 邮件短信微服务,解决国际化服务的国内外兼容性问题,含各服务商调研情况 邮件短信微服务 spring boot 微服务 接收json格式参数 验证参数合 ...

  6. 【Other】最近在研究的, Java/Springboot/RPC/JPA等

    我的Springboot框架,欢迎关注: https://github.com/junneyang/common-web-starter Dubbo-大波-服务化框架 dubbo_百度搜索 Dubbo ...

  7. Reactive 理解 SpringBoot 响应式的核心-Reactor

    Reactive 理解 SpringBoot 响应式的核心-Reactor bestcoding 2020-02-23 17:26:43 一.前言 关于 响应式 Reactive,前面的两篇文章谈了不 ...

  8. Java实现爬取京东手机数据

    Java实现爬取京东手机数据 最近看了某马的Java爬虫视频,看完后自己上手操作了下,基本达到了爬数据的要求,HTML页面源码也刚好复习了下,之前发布两篇关于简单爬虫的文章,也刚好用得上.项目没什么太 ...

  9. Java SpringBoot 项目构建 Docker 镜像调优实践

    PS:已经在生产实践中验证,解决在生产环境下,网速带宽小,每次推拉镜像影响线上服务问题,按本文方式构建镜像,除了第一次拉取.推送.构建镜像慢,第二.三-次都是几百K大小传输,速度非常快,构建.打包.推 ...

  10. 谈谈Java利用原始HttpURLConnection发送POST数据

    这篇文章主要给大家介绍java利用原始httpUrlConnection发送post数据,设计到httpUrlConnection类的相关知识,感兴趣的朋友跟着小编一起学习吧 URLConnectio ...

随机推荐

  1. itest(爱测试) 4.5.7 发布,开源BUG 跟踪管理 & 敏捷测试管理&极简项目管理软件

    itest 简介 itest 开源敏捷测试管理,testOps 践行者,极简的任务管理,测试管理,缺陷管理,测试环境管理,接口测试5合1,又有丰富的统计分析.可按测试包分配测试用例执行,也可建测试迭代 ...

  2. redhat8 添加scsi类型的硬盘后开机重配虚拟机 且黑屏

    原因:因为vmware磁盘优先级里nvme类型的磁盘比scsi类型的磁盘优先级高,所以开机先启动scsi类型的磁盘 又因为scsi并不是系统盘所以不能够实现开机 解决方法: 改变vmware的磁盘优先 ...

  3. LeetCode 449. Serialize and Deserialize BST 序列化和反序列化二叉搜索树 (Java)

    题目: Serialization is the process of converting a data structure or object into a sequence of bits so ...

  4. CSPJ赛前刷题

    T1 \(\color{red}\text{正难则反}\),最短路 T2 图论(糅杂着一点DP) T3 DP 优化:减去不需要的状态 T4 一定要写注释!!! 不开longlong见祖宗!!! T5 ...

  5. 喜讯!INFINI Easysearch 在墨天轮数据库排名中挺进前30!

    近日,2023 年 10 月的 墨天轮中国数据库流行度排行 火热出炉,本月共有 283 个数据库参与排名,中国数据库行业竞争日益激烈.其中,极限科技旗下软件产品 INFINI Easysearch 稳 ...

  6. json null (空值)不序列化

    json null (空值)不序列化 JsonSerializerSettings jsetting = new JsonSerializerSettings(); jsetting.DefaultV ...

  7. Vue学习:3.V标签综合3

    接上一篇... V标签综合使用:记事本案例 功能: 在实现列表渲染和删除功能的基础上,增加了数据添加以及数据统计,同时要求底部统计和清空区域在事件数目为0时不显示. 思路: 整体架构分为三部分:头部使 ...

  8. CM 停用 Parcel 异常

    在将Doris集成到CM时,第一次打的包存在问题,想更新下,停用.删除Parcel时出现了问题卡住了,一直显示75%.无奈换了名称和版本,分配.激活,然后又卡在了75%,点开后,发现是同一台机器.其a ...

  9. python logger 打印日志错误行数

    python logger 打印日志错误行数 import logging app = Flask(__name__) # 配置日志 handler = logging.FileHandler('ap ...

  10. 前端学习之nvm

    接手了新的项目 需要使用nodejs,但是版本又不同如何解决呢 如果自己下载配置环境变量也太复杂了 下载nvm https://nvm.uihtm.com/download.html 使用nvm 下载 ...