实现效果:

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. kettle从入门到精通 第五十三课 ETL之kettle MQTT/RabbitMQ consumer实战

    1.上一节课我们学习了MQTT producer 生产者步骤,MQTT consumer消费者步骤.该步骤可以从支持MRQTT协议的中间件获取数据,该步骤和kafka consumer 一样可以处理实 ...

  2. Spring扩展——@Import注解

    引言 在Spring中有许多Enable开头的注解,比如以下常见注解 @EnableTransactionManagement @EanbleAsync @EnableCache @EnableAsp ...

  3. 在线Base64转文件、文件转Base64工具

    在线Base64转换神器,一键实现Base64编码与文件互转!支持图片.文档等各类文件,快速准确,安全无服务器存储.拖拽上传,轻松编码解码,提升开发效率.跨平台兼容,移动端友好,让数据转换再无障碍. ...

  4. 「C++」简单模拟

    这是一个公式: \[F_n=\dfrac{\left(\frac{1+\sqrt{5}}{2}\right)^n-\left(\frac{1-\sqrt{5}}{2}\right)^n}{\sqrt{ ...

  5. python selenium.remote远程连接时间达10分钟

    问题: 在机器A搭建了selenium-grid的环境,hub以4444端口号启动,并在4444下注册了子node,端口4445,浏览器配置chrome 使用代码进行远程连接,并创建会话: 传入的se ...

  6. springboot支持http2

    现在http/3都出来了,但是很多项目还是没有采用https,这个是说不过去的. http3在2022/06/06 正式发布,具体见https://www.163.com/dy/article/H9B ...

  7. GitHub上狂揽62Kstars的程序员做饭指南

    写在前面 作为一名程序员,我们经常会在全球最大的同性交友网站 GitHub上寻找一些优质的开源项目来学习,以提升自己的专业技能. 今天给大家推荐的是一个GitHub上狂揽61K的开源项目.项目名叫做H ...

  8. ClickHouse介绍(四)ClickHouse使用操作

    ClickHouse使用操作 这章主要介绍在ClickHouse使用的各个操作的注意点.常规的统一语法不做详细介绍. 1. Join操作 在ClickHouse中,对连接操作定义了不同的精度,包含AL ...

  9. 一文带你深入理解SpringMVC的执行原理

    今天大致来看一下Spring MVC的执行流程是什么样的 执行流程:也就是一个请求是怎么到我们Controller的,返回值是怎么给客户端的 本文分析的问题: 文件上传的请求是怎么处理的 跨域是怎么处 ...

  10. C#/.NET/.NET Core优秀项目和框架2024年6月简报

    前言 公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(每周至少会推荐两个优秀的项目和框架当然节假日除外),公众号推文中有项目和框架的介绍.功能特点.使用方式以及部分功能截图 ...