实现效果:

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. STM32 USB CDC调试记录

    STM32 USB CDC调试 一.前言 最近在做STM32的IAP方案,官方提供的demo是基于USART实现,但是使用USART的话要和电脑通信要么借助USB转TTL工具:要么在板子上加一颗CH3 ...

  2. MySQL学习笔记-存储引擎

    存储引擎 一. MySQL体系结构 MySQL Server 连接层:连接的处理.认证授权.安全方案.检查是否超过最大连接数等. 服务层:SQL接口.解析器.查询优化器.缓存 引擎层:引擎是数据存储和 ...

  3. 【AI应用开发全流程】使用AscendCL开发板完成模型推理

    本文分享自华为云社区<[昇腾开发全流程]AscendCL开发板模型推理>,作者:沉迷sk. 前言 学会如何安装配置华为云ModelArts.开发板Atlas 200I DK A2.并打通一 ...

  4. kettle从入门到精通 第五十七课 ETL之kettle调用存储过程

    1.之前有个同学说他使用kettle执行一坨sql语句时,kettle直接卡死掉了.我给出的建议是使用存储过程,果不其然使用存储过程顺利解决问题. 今天我们一起来学习下kettle如何调用存储过程,这 ...

  5. linux系统下,安装mysql教程

    1.查看mysql是否安装 首先我们需要查看软件是否已经安装,或者说查看安装的软件包名称.如查找是否安装mysql rpm -qa | grep mysql 2.卸载查到的软件 rpm -e --no ...

  6. ABC317题解报告

    我直接从第三题开始讲了. T3 把数组 \(A\) 从大到小排序. 然后从前往后把前 \(q\) 个数加起来,然后判断这 \(q\) 个数的和与 \(d\) 的大小关系,如果大了就变成 \(d\). ...

  7. 手写LRU热点缓存数据结构

    引言 LRU是开发过程中设计缓存的常用算法,在此基础上,如何设计一个高效的缓存呢?本文就带大家分析并手撸一个LRUCache. LRU算法 LRU(Least recently used,最近最少使用 ...

  8. ARC 170_A 题解

    AT_arc170_a [ARC170A] Yet Another AB Problem 题解 这道题做了我七天 (同时也是我第一到通过的 ARC 题) 太酷了 其实还是比较好理解的 原题题干 原题题 ...

  9. AtCoder Beginner Contest 302 H. Ball Collector 题解 可撤销并查集

    为了更好的阅读体验,请单击这里 AtCoder Beginner Contest 302 H. Ball Collector 题意跳过. 可以视作将 \(a_i, b_i\) 之间连了一条边,然后 \ ...

  10. Android7.0 配置JACK支持多用户同时编译

    # Android7.0 配置JACK支持多用户同时编译 reference: https://blog.csdn.net/whorus1/article/details/80364772 https ...