【Java开发】基于AOP切面的数据脱敏

news/2025/1/5 7:33:10/文章来源:https://www.cnblogs.com/o-O-oO/p/18648926

#数据安全 #AOP切面编程 #数据脱敏 #微服务架构

随着数据安全法个人信息保护法的实施,我们都知道,保护数据是企业信息化建设中非常重要的事情。

数据的生命周期包括:采集、传输、存储、处理、交换、销毁6个阶段

DSMM定义了数据生命周期中每个阶段需要做的一些安全控制,比如采集阶段进行数据分级,传输阶段采用加密传输协议,存储阶段使用强加密算法对敏感数据进行加密存储,处理阶段实施脱敏以及访问控制,交换阶段使用隐私计算,销毁阶段使用净化技术使数据无法被任何技术手段恢复等。

当然每个阶段不仅仅只有上面描诉的安全控制,以及并不是哪个阶段必须要使用这些安全控制。比如不同组织之间的数据交换过程,并非要使用隐私计算,可以通过其他的安全策略达到组织想要的安全目的,可以是技术性控制也可以是管理性控制/行政控制。

本文的重点是如何在微服务架构中通过AOP切面编程实现敏感数据脱敏,针对其他的内容不过多阐述,比如访问控制、加解密的技术实现都可以单独的写一篇文章。

当前微服务架构,一个系统拆分成很多的模块进行开发。每个模块的开发人员只需要写和业务相关的代码即可,比如订单模块、优惠券模块、商品模块、资产模块都可以看作是单独的应用。各个模块之间功能使用HTTP或RPC协议进行调用,各个微服务应用之间可以按需组合。比如订单模块和优惠券模块组合成一个链路,比如优惠券、资产、订单三个应用组合成一个链路,链路的入口是API网关。

数据安全发展至今,脱敏技术分为“静态脱敏”和“动态脱敏“两种。静态脱敏由于改变了元数据,对业务影响很大,所以现在市面上大部分脱敏产品使用的是动态脱敏技术,即边脱敏边使用,并不会修改原有数据内容。

由于业务系统越来越复杂,一个系统中的多个app对数据的访问需求是不一样的。比如订单应用需要查看用户手机号明文,而资产应用需要看到用户脱敏的手机号。

所以,脱敏可以在单个应用中实现,也可以在Api gateway中实现,也可以在数据库实现,但每种实现方式都有自己的优缺点。

在生产环境中,一般有2种选择,单个应用的逻辑中进行脱敏或在Api网关中实现脱敏逻辑。

应用自身进行脱敏,优点是可以根据业务的数据访问需求,自定义自己的数据访问控制和脱敏方案。缺点是写业务的程序员在写业务逻辑之外增加了额外的开发成本。

另外一种方式是网关层脱敏

网关层脱敏的优点是,无需侵入业务代码逻辑,不会增加单个应用的开发成本。而缺点是,无法根据每个应用对数据的安全需求进行权限控制和脱敏方式选择。网关层并不知道每个应用中比如app1、app2中需要脱敏的实体类中的对象,比如app1中的Person中的name对象、phone对象,app2中的address对象,所以在网关层脱敏是比较难实现的一件事。

我在两家甲方待过,不论哪一家,我的脱敏方案都是业务层+动态脱敏。

我封装好一个脱敏工具包,包含对姓名,手机号,身份证,电子邮箱,地址,银行卡号进行脱敏。

1、先定义个需要脱敏的枚举实体类

@Getter
@AllArgsConstructor
public enum ReadableSensitiveTypeEnum {/*** 身份证编号*/ID_CARD("身份证"),/*** 地址/住址*/ADDRESS("地址"),/*** 姓名*/NAME("姓名"),/*** 手机号*/PHONE("手机号"),/*** 手机号*/EMAIL("邮箱"),/*** 银行卡号*/BANK_CARD_NO("银行卡号");private String desc;}

2、定义一个需要脱敏的注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ReadableSensitiveVerify {ReadableSensitiveTypeEnum value();}

3、然后再编写一个脱敏工具类


public class DesensitizationUtils {/*** @description: 名字脱敏* 脱敏规则: 隐藏中中间部分,比如:李某人 置换为 李*人 , 李某置换为 *某,司徒司翘置换为 司**翘*/public static String desensitizedName(String fullName){if (!Strings.isNullOrEmpty(fullName)) {int length = fullName.length();if(length == 2){return "*".concat(fullName.substring(1));}else if(length == 3){return StringUtils.left(fullName,1).concat("*").concat(StringUtils.right(fullName,1));}else if(length > 3){return StringUtils.left(fullName,1).concat(generateAsterisk(fullName.substring(1,length-1).length())).concat(StringUtils.right(fullName,1));}else {return fullName;}}return fullName;}/*** @description: 手机号脱敏,脱敏规则: 保留前三后四, 比如15566026528置换为155****6528*/public static String desensitizedPhoneNumber(String phoneNumber){if(StringUtils.isNotEmpty(phoneNumber)){int length = phoneNumber.length();if(length == 11){return phoneNumber.replaceAll("(\\w{3})\\w*(\\w{4})", "$1****$2");}else if(length > 2){return StringUtils.left(phoneNumber,1).concat(generateAsterisk(phoneNumber.substring(1,length-2).length())).concat(StringUtils.right(phoneNumber,1));}else {return phoneNumber;}}return phoneNumber;}/*** @description: 身份证脱敏* 脱敏规则: 保留前六后三, 适用于15位和18位身份证号:* 原身份证号(15位):210122198401187,脱敏后的身份证号:210122******187* 原身份证号(18位):210122198401187672,脱敏后的身份证号:210122*********672*/public static String desensitizedIdNumber(String idNumber){if (!Strings.isNullOrEmpty(idNumber)) {int length = idNumber.length();if (length == 15){return idNumber.replaceAll("(\\w{6})\\w*(\\w{3})", "$1******$2");}else if (length == 18){return idNumber.replaceAll("(\\w{6})\\w*(\\w{3})", "$1*********$2");}else if(length > 9){return StringUtils.left(idNumber,6).concat(generateAsterisk(idNumber.substring(6,length-3).length())).concat(StringUtils.right(idNumber,3));}}return idNumber;}/*** @description: 电子邮箱脱敏,脱敏规则:电子邮箱隐藏@前面的3个字符*/public static String desensitizationEmail(String email) {if (StringUtils.isEmpty(email)) {return email;}String encrypt = email.replaceAll("(\\w+)\\w{3}@(\\w+)", "$1***@$2");if (email.equalsIgnoreCase(encrypt)) {encrypt = email.replaceAll("(\\w*)\\w{1}@(\\w+)", "$1*@$2");}return encrypt;}/*** @description: 地址脱敏,脱敏规则:从第4位开始隐藏,隐藏8位*/public static String desensitizedAddress(String address){if (!Strings.isNullOrEmpty(address)) {int length = address.length();if(length > 4 && length <= 12){return StringUtils.left(address, 3).concat(generateAsterisk(address.substring(3).length()));}else if(length > 12){return StringUtils.left(address,3).concat("********").concat(address.substring(11));}else {return address;}}return address;}/*** @description: 银行账号脱敏, 脱敏规则:银行账号保留前六后四*/public static String desensitizedAddressBankCardNum(String acctNo) {if (StringUtils.isNotEmpty(acctNo)) {String regex = "(\\w{6})(.*)(\\w{4})";Matcher m = Pattern.compile(regex).matcher(acctNo);if (m.find()) {String rep = m.group(2);StringBuilder sb = new StringBuilder();for (int i = 0; i < rep.length(); i++) {sb.append("*");}acctNo = acctNo.replaceAll(rep, sb.toString());}}return acctNo;}/*** @description: 返回指定长度*字符串*/private static String generateAsterisk(int length){String result = "";for (int i = 0; i < length; i++) {result += "*";}return result;}
}

然后在app1、和app2中只需要导入脱敏包,对实体类中需要进行脱敏的字段添加注解。

@Data
@EqualsAndHashCode()
@ApiModel(value="CustomerInfoListVo对象", description="个人客户列表信息")
public class CustomerInfoListVo implements Serializable {@ApiModelProperty(value = "记录id")private Integer id;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")@ApiModelProperty(value = "创建时间")private Date createTime;@ApiModelProperty(value = "客户名称")@ReadableSensitiveVerify(ReadableSensitiveTypeEnum.NAME)private String realName;@ApiModelProperty(value = "手机号码")@ReadableSensitiveVerify(ReadableSensitiveTypeEnum.PHONE)private String phone;@ApiModelProperty(value = "客户身份证号")@ReadableSensitiveVerify(ReadableSensitiveTypeEnum.ID_CARD)private String idCarNumber;@ApiModelProperty(value = "性别")private String gender;@ApiModelProperty(value = "出生日期")@JsonFormat(pattern = "yyyy-MM-dd",timezone="GMT+8")private Date birthDate;@ApiModelProperty(value = "客户编号")private String customerCode;@ApiModelProperty(value = "邮箱")private String email;@ApiModelProperty(value = "客户经理")private String customerManager;@ApiModelProperty(value = "微信号")private String weChat;@ApiModelProperty(value = "拉黑时间")@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")private Date blockingTime;@ApiModelProperty(value = "拉黑说明")private String blockInstructions;@ApiModelProperty(value = "意向等级")private String interestingGrade;
}

最后在业务代码中编写一个切面类

/**
* @description: 返回值数据脱敏处理aop
*/
@Slf4j
@Component
@Aspect
public class DesensitizationAspect {@Autowiredprivate UserUtils userUtils;/*** @description: 切入点* @return:* @author: Ming* @time: 2022/6/22*/@Pointcut("execution(* com.yptx.financialsystem.*.controller.*.*(..))")public void pointCut() {}/*** @description: 返回值处理* @return:* @author: Ming* @time: 2022/6/22*/@Around("pointCut()")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {SysUser user = userUtils.getSysUser();log.info("user: {}",user);//typeEnums表示用户拥有哪些字段的查看权限,没有的就脱敏处理List<ReadableSensitiveTypeEnum> typeEnums = new ArrayList<>();if(user != null){typeEnums = user.getReadableSensitives() == null ? typeEnums : user.getReadableSensitives();}log.info("typeEnums: {}",typeEnums);Object obj = proceedingJoinPoint.proceed();if (obj == null || isPrimitive(obj.getClass())) {return obj;}dealData(obj,typeEnums);return obj;}/*** @description: 基本数据类型和String类型判断* @return:* @author: Ming* @time: 2022/6/23*/private boolean isPrimitive(Class<?> clz) {try {if (String.class.isAssignableFrom(clz) || clz.isPrimitive()) {return true;} else {return ((Class) clz.getField("TYPE").get(null)).isPrimitive();}} catch (Exception e) {return false;}}/*** @description: 数据处理* @return:* @author: Ming* @time: 2022/6/23*/private void dealData(Object obj,List<ReadableSensitiveTypeEnum> typeEnums){if (null == obj) {return;}if (obj.getClass().isPrimitive()) {return;}// 是否是接口if (obj.getClass().isInterface()) {return;}Object data = ((ResponseJson) obj).getObj();if(data != null){Class<?> clazz = data.getClass();if (clazz.equals(Page.class)) {Page page = (Page) data;List<?> record = page.getRecords();for (Object o : record) {Field[] fields = o.getClass().getDeclaredFields();replace(fields,o,typeEnums);}}else {Field[] fields = clazz.getDeclaredFields();replace(fields,data,typeEnums);}}}/*** @description: 脱敏敏感字段* @return:* @author: Ming* @time: 2022/6/23*/private void replace(Field[] fields,Object o,List<ReadableSensitiveTypeEnum> typeEnums){try {for (Field f : fields) {if(f != null){//设置private字段可访问f.setAccessible(true);//处理自定义vo作为属性(属性类型非自身类对象类型的属性)CustomEntityDesensitizationVerify custom = f.getAnnotation(CustomEntityDesensitizationVerify.class);if(custom != null){Object customEntity = f.get(o);Field[] entityFiled = customEntity.getClass().getDeclaredFields();replace(entityFiled, customEntity,typeEnums);}//处理list属性Class<?> curFieldType = f.getType();if (curFieldType.equals(List.class)) {List<?> record = (List<?>) f.get(o);if(record != null && !record.isEmpty() ){for (Object obj :record) {Field[] ff= obj.getClass().getDeclaredFields();replace(ff,obj,typeEnums);}}}//处理普通字符串字段ReadableSensitiveVerify annotation = f.getAnnotation(ReadableSensitiveVerify.class);if(annotation != null){f.getType();String valueStr = (String) f.get(o);if(StringUtils.isNotEmpty(valueStr)){ReadableSensitiveTypeEnum type = annotation.value();if(type.equals(ReadableSensitiveTypeEnum.NAME) && !typeEnums.contains(type)){f.set(o, DesensitizationUtils.desensitizedName(valueStr));}if(type.equals(ReadableSensitiveTypeEnum.ID_CARD) && !typeEnums.contains(type) ){f.set(o, DesensitizationUtils.desensitizedIdNumber(valueStr));}if(type.equals(ReadableSensitiveTypeEnum.ADDRESS) && !typeEnums.contains(type)){f.set(o, DesensitizationUtils.desensitizedAddress(valueStr));}if(type.equals(ReadableSensitiveTypeEnum.PHONE) && !typeEnums.contains(type)){f.set(o, DesensitizationUtils.desensitizedPhoneNumber(valueStr));}if(type.equals(ReadableSensitiveTypeEnum.BANK_CARD_NO) && !typeEnums.contains(type)){f.set(o, DesensitizationUtils.desensitizedAddressBankCardNum(valueStr));}if(type.equals(ReadableSensitiveTypeEnum.EMAIL) && !typeEnums.contains(type)){f.set(o, DesensitizationUtils.desensitizationEmail(valueStr));}}}}}}catch (Exception e){e.printStackTrace();}}
}

具体的效果

总结

业务要实现数据安全的要求,不是一个脱敏就能解决问题的,动态脱敏技术只是在数据处理过程中返回给的用户的数据是脱敏的,但存储在数据库中的静态数据仍然是明文的,从威胁建模的角度而言,仍然存在敏感数据泄漏的风险,所以对数据分级的最高密级数据要做加密存储是必要的,并且需要通过某些安全措施保证加解密的密钥不会泄漏。

原创 信息安全笔记

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/863143.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《计算机组成及汇编语言原理》阅读笔记:p177-p177

《计算机组成及汇编语言原理》学习第 13 天,p177-p177 总结,总计 1 页。 一、技术总结 1.real mode A programming model where the program has access to the entire capability of the machine, bypassing security and memory management. Useful primarily for operat…

Android Password Safe(密码保险箱)

password safe手机版是一款主打密码管理跟保存内容的软件,密码保险箱与管理器以加密方式存储和管理你输入的所有数据,因此你可以安全存储访问数据,你只需要记住主密码。 此密码管理器允许你管理和跟踪所有敏感数据,这些数据完全加密且安全存储。 用于保护数据保险库的加密基…

Android GKD(自定义屏幕点击) v1.9.3

GKD是一款基于无障碍、高级选择器和订阅规则的自定义屏幕点击APP,为用户提供了更加便捷和智能的交互体验。通过点击跳过任意开屏广告或关闭应用内部任意弹窗广告,用户可以迅速进入应用的核心功能,而无需被广告打断。获取地址:https://www.dmjf.top/2678.html

Win32汇编学习笔记02.RadAsm和联合编译

https://bpsend.net/thread-151-1-1.html汇编使用资源 汇编使用资源的方式和C的一样,也是把资源文件 rc 编译成 res 再链接进去,汇编没有自己的资源编辑器,需要借助 vc6.0或者 vs 主要是把 头文件 .h 转化为对应的 .inc 使用vc6.0建立资源文件用vs建立资源文件新建一个桌面向导…

判断方法的使用范围20250102

判断方法的使用范围20250102package com.oop.demo01;public class Student {public static int add(int a, int b) { //public 类下的public的静态方法return a+b;}static int sub(int a, int b) { //public类 非public的静态方法return a-b;}public int devide(int a, int…

火绒(杀毒软件) v6.0.1.4 正式版

火绒这个软件很受极客们的欢迎,是一款小巧轻便的杀毒软件,功能也很丰富,运行安静,使用方便。也是果核很喜欢的一款杀毒软件,其火绒剑是一款非常强大的系统监控和调试工具,能帮助有电脑底层基础的用户快速排查问题。获取地址:https://www.dmjf.top/2255.html

Android AdGuard(广告拦截) v4.7.163 高级版

Adguard为你提供了一个可靠的、可管理的保护,就没有你们的参与滤波器加载网页。adguard移除所有烦人的广告,阻止危险网站的加载,也不会允许任何人在网上跟踪你的活动。获取地址:https://www.dmjf.top/2690.html

Windows编译QT6.4.3及使用

1. 下载QT6.4.3源码,并解压 Index of /archive/qt/6.4/6.4.3/singlehttps://download.qt.io/archive/qt/6.4/6.4.3/single/ 2.安装环境 * CMake 3.18 or later* Perl 5.8 or later* Python 2.7 or later* C++ compiler supporting the C++17 standard 3.打开windows的cmd cd…

ESP32-S3-N16R8在platformio中的开发板设置

前言 platformio现有的板子库里面没有ESP32-S3-N16R8(8MB PSRAM + 16MB FLASH)的开发板模型,直接强行套用,要么就是解锁不了8MB PSRAM,要么就下载后运行不起来。一、选用esp32-s3-devkitc-1开发板 先选用esp32-s3-devkitc-1作为开发板模型,点击Finish后务必耐心等待。 二…

《docker基础篇:8.Docker常规安装简介》包括:docker常规安装总体步骤、安装tomcat、安装mysql、安装redis

《docker基础篇:8.Docker常规安装简介》包括:docker常规安装总体步骤、安装tomcat、安装mysql、安装redis@目录8.Docker常规安装简介8.1 docker常规安装总体步骤8.2安装tomcat8.3 安装mysql8.3.1 docker hub上面查找mysql镜像8.3.2 从docker hub上(阿里云加速器)拉取mysql镜像…

Elasticsearch VS Easysearch 性能测试

压测环境 虚拟机配置 使用阿里云上规格:ecs.u1-c1m4.4xlarge,PL2: 单盘 IOPS 性能上限 10 万 (适用的云盘容量范围:461GiB - 64TiB)vCPU 内存 (GiB) 磁盘(GB) 带宽(Gbit/s) 数量16 64 500 5000 24Easysearch 配置 7 节点集群,版本:1.9.0实例名 内网 IP 软件 vCPU JVM 磁…

win10/win11 用 ncpa.cpl 命令快速打开网络连接

前言:Win11系统配置网络适配器好费劲的,每次都要在设置找半天 得,直接来,快捷键安排1、开始 -> 运行 Win + R 弹出 运行 窗口2、输入命令 ncpa.pcl并回车 3、见证奇迹QQ:1061767621 Q群:215481318