实现效果:

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. 小程序的文件结构及配置 小程序配置 app.json

    程序包含一个描述整体程序的 app 和多个描述各自页面的 page. 一个小程序主体部分由三个文件组成,必须放在项目的根目录,如下: 文件 必填 作用 app.js 是 小程序逻辑-小程序入口文件 a ...

  2. 铭瑄 USB 供电不足

    铭瑄 USB 供电不足 可能是USB固件开了节能,节能状态和某些设备会不兼容,更新固件试试固件链接:链接:https://pan.baidu.com/s/1RxHEddYe6TWMDlMJ3PQB1Q ...

  3. 手机上玩 PC 游戏的开源项目「GitHub 热点速览」

    上周国产 3A 大作<黑神话:悟空>开启预售,同时公布游戏将于北京时间 2024.8.20 正式上线.这是一款由「游戏科学」开发的西游题材单机·动作·角色扮演游戏,它采用「虚幻引擎5」制作 ...

  4. 硬件开发笔记(十九):Altium Designer 21软件介绍和安装过程

    前言   AD硬件设计软件之一,前面说了allego,但是allego对项目的管理.原理图生成PCB,PCB反向原理图等方面比较复杂,对于一般的硬件(非多个高速电路),选择AD能够加大的节省开发工作量 ...

  5. 大模型重塑软件开发,华为云AI原生应用架构设计与实践分享

    在ArchSummit全球架构师峰会2024上,华为云aPaaS平台首席架构师马会彬受邀出席,和技术爱好者分享AI原生应用引擎的架构与实践. AI大模型与AI重塑软件的大趋势下,软件会发生哪些本质的变 ...

  6. Linux 内核:设备驱动模型(3)class与device

    Linux 内核:设备驱动模型(3)class与device 背景 前面我们知道了设备如何通过总线与驱动匹配,也了解了设备插拔时与用户空间是如何通过uevent基于环境变量进行交互的. 前面看过了设备 ...

  7. 【进阶篇】Java 项目中对使用递归的理解分享

    [进阶篇]Java 项目中对使用递归的理解分享 目录 [进阶篇]Java 项目中对使用递归的理解分享 前言 一.什么是递归 1.1基本概念 1.2优缺点 1.3与迭代的区别 二.实际案例 三.改进方案 ...

  8. 缩小50%,Mini版T3/A40i核心板,让您的设备更小巧!

    小尺寸核心板给用户带来何种价值? 创龙科技常收到用户对于小尺寸核心板的需求反馈,尤其在电力数据采集器.电力DTU.电力通讯管理机.运动控制器.工业HMI.工业网关等工业设备中. 小尺寸核心板3大优势将 ...

  9. java.net.UnknownHostException: api.weixin.qq.com解决办法

    java.net.UnknownHostException: api.weixin.qq.com at java.net.AbstractPlainSocketImpl.connect(Abstrac ...

  10. 推荐一款功能强大、界面优美的开源SSH跨平台终端软件WindTerm

    WindTerm是一款开源免费且功能强大的终端软件,相比 MobaXterm自带中文支持.无论是在Windows.macOS还是Linux操作系统上,WindTerm都能提供出色的性能和稳定性.Win ...