成都建设网站那家好,软文营销的经典案例,淮安维度网站建设,企业网站报价方案模板文章目录 引入认识 YAML 格式规范定义脱敏规则格式脱敏逻辑实现读取 YAML 配置文件获取脱敏规则通过键路径获取对应字段规则原始优化后 对数据进行脱敏处理递归生成字段对应的键路径脱敏测试 完整工具类 引入
在项目中遇到一个需求#xff0c;需要对交易接口返回结果中的指定… 文章目录 引入认识 YAML 格式规范定义脱敏规则格式脱敏逻辑实现读取 YAML 配置文件获取脱敏规则通过键路径获取对应字段规则原始优化后 对数据进行脱敏处理递归生成字段对应的键路径脱敏测试 完整工具类 引入
在项目中遇到一个需求需要对交易接口返回结果中的指定字段进行脱敏操作但又不能使用AOP注解的形式于是决定使用一种比较笨的方法
首先将所有需要脱敏字段及其对应脱敏规则存储到 Map 中。在接口返回时遍历结果中的所有字段判断字段名在 Map 中是否存在 如果不存在说明该字段不需要脱敏不做处理即可。如果存在说明该字段需要脱敏从 Map 中获取对应的脱敏规则进行脱敏。 最后返回脱敏之后的结果。
认识 YAML 格式规范
由于返回的结果涉及到嵌套 Map所以决定采用 YAML 格式的文件存储脱敏规则那么为了大家统一维护和开发就需要大家对 YAML 格式进行了解遵守规范不易出错少走弯路。
YAMLYAML Ain’t Markup Language与传统的 JSON、XML 和 Properties 文件一样都是用于数据序列化的格式常用于配置文件和数据传输。
相比于其他格式YAML 是一种轻量级的数据序列化格式它的设计初衷是为了简化复杂性提高人类可读性并且易于实现和解析。 与 JSON 相比YAML 在语法上更为灵活允许使用更简洁的方式来表示数据结构。 与 XML 相比YAML 的语法更为简洁没有繁琐的标签和尖括号。 与 Properties 相比YAML 支持更复杂的数据结构包括嵌套的键值对和列表。
除此之外YAML 还支持跨平台、跨语言可以被多种编程语言解析这使得YAML非常适合用于不同语言之间的数据传输和交换。
YAML 文件的语法非常简洁明了以下是它的语法规范 基本语法 使用 缩进表示层级关系可以使用空格或制表符进行缩进但不能混用。使用冒号:表示键值对键值对之间使用换行分隔。使用破折号-表示列表项列表项之间也使用换行分隔。 # 使用缩进表示层级关系
server:port: 8080# 使用冒号表示键值对
name: John Smith
age: 30# 使用破折号表示列表项
hobbies:- reading- hiking- swimming注释 使用井号#表示注释在 # 后面的内容被视为注释可以出现在行首或行尾。 # 这是一个注释
name: John Smith
age: 30 # 这也是一个注释字符串: 字符串可以使用单引号或双引号括起来也可以不使用引号。使用双引号时可以使用转义字符如 \n 表示换行和转义序列如 \u 表示 Unicode 字符。 # 使用双引号表示字符串
name: John Smith# 使用单引号表示字符串
nickname: Johnny键值对 键值对使用冒号:表示键和值之间使用一个 空格 分隔。键可以是字符串或纯量如整数、布尔值等。值可以是字符串、纯量、列表或嵌套的键值对。 # 键和值之间使用一个空格分隔
name: John Smith# 键可以是字符串或纯量
age: 30# 值可以是字符串、纯量、列表或嵌套的键值对
address:city: San Franciscostate: Californiazip: 94107列表 使用破折号-表示列表项。列表项可以是字符串、纯量或嵌套的列表或键值对。 # 使用破折号表示列表项
hobbies:- reading- hiking- swimming# 列表项可以是字符串、纯量或嵌套的列表或键值对
people:- name: John Smithage: 30- name: Jane Doeage: 25引用 使用表示引用使用*表示引用的内容。 # 使用表示引用
address: myaddresscity: San Franciscostate: Californiazip: 94107# 使用*表示引用的内容
shippingAddress: *myaddress多行文本块 使用|保留换行符保留文本块的精确格式。使用折叠换行符将文本块折叠成一行并根据内容自动换行。 # 使用|保留换行符
description: |This is amulti-linestring.# 使用折叠换行符
summary: This is a summarythat may containline breaks.数据类型 YAML支持多种数据类型包括字符串、整数、浮点数、布尔值、日期和时间等。可以使用标记来表示一些特殊的数据类型如 !!str 表示字符串类型、!!int 表示整数类型等。 # 使用标记表示数据类型
age: !!int 30
weight: !!float 65.5
isMale: !!bool true
created: !!timestamp 2022-01-01 12:00:00多文件 可以使用—表示多个 YAML 文件之间的分隔符。每个文件可以使用任何 YAML 语法。 # 第一个YAML文件
name: John Smith
age: 30---# 第二个YAML文件
hobbies:- reading- hiking- swimming定义脱敏规则格式
对于数据结构简单的接口返回结果脱敏规则格式定义为【交易号-字段-规则】
交易号:字段名:规则: /^(1[3-9][0-9])\d{4}(\d{4}$)/同时接口返回的结果中可能用有嵌套列表那么针对这种复杂的结构就定义格式为【交易号-字段列表-字段-规则】即
交易号:字段名(列表):字段名:规则: /^(1[3-9][0-9])\d{4}(\d{4}$)/使用这种层级结构我们完全可以通过 Map.get(Key) 的形式获取到指定交易指定字段的脱敏规则。
脱敏逻辑实现
读取 YAML 配置文件获取脱敏规则 首先创建 YAML 文件 desensitize.yml 添加对应交易字段的脱敏规则 Y3800:phone:rule: (\\d{3})\\d{4}(\\d{4})format: $1****$2idCard:rule: (?\\w{6})\\w(?\\w{4})format: *
Y3801:idCard:rule: (?\\w{3})\\w(?\\w{4})format: list:phone:rule: (\\d{3})\\d{4}(\\d{4})format: $1$2定义脱敏工具类 DataDesensitizationUtils 编写我们的脱敏逻辑 public class DataDesensitizationUtils {
}在 DataDesensitizationUtils 工具类中我们需要实现在项目启动时读取 desensitize.yml 文件中的内容并转为我们想要的 Map 键值对数据类型 /*** 读取yaml文件内容并转为Map* param yamlFile yaml文件路径* return Map对象*/
public static MapString, Object loadYaml(String yamlFile) {Yaml yaml new Yaml();try (InputStream in DataDesensitizationUtils.class.getResourceAsStream(yamlFile)) {return yaml.loadAs(in, Map.class);} catch (Exception e) {e.printStackTrace();}return null;
}在上述代码中我们通过 getResourceAsStream 方法根据指定的 YAML 文件的路径从类路径中获取资源文件的输入流。 然后使用 loadAs 方法将输入流中的内容按照 YAML 格式进行解析并将解析结果转换为指定的 Map.class 类型。 最后使用 try-with-resources 语句来自动关闭输入流。
通过键路径获取对应字段规则
原始 在上文中我们已经将 desensitize.yml 文件中所有的脱敏规则都以 key-Value 的形式存储到了 Map 中因此我们只需要通过 Key 从 Map 中获取即可。接下来编写方法通过 Key 获取指定字段对应脱敏规则 public static void main(String[] args) {// 加载 YAML 文件并获取顶层的 Map 对象路径基于 resources 目录MapString, Object yamlMap loadYaml(/desensitize.yml);System.out.println(yamlMap);// 从顶层的 Map 中获取名为 Y3800 的嵌套 MapMapString, Object Y3800 (MapString, Object) yamlMap.get(Y3800);System.out.println(Y3800);// 从 Y3800 的嵌套 Map 中获取名为 phone 的嵌套 MapMapString, Object phone (MapString, Object) Y3800.get(phone);System.out.println(phone);
}输出结果如下 {Y3800{phone{rule(\d{3})\d{4}(\d{4}), format$1****$2}, idCard{rule(?\w{3})\w(?\w{4}), format*}}, Y3801{name{rule.(?.), format}, idCard{rule(?\w{3})\w(?\w{4}), format}, list{card{rule\d(?\d{4}), format}}}}
{phone{rule(\d{3})\d{4}(\d{4}), format$1****$2}, idCard{rule(?\w{3})\w(?\w{4}), format*}}
{rule(\d{3})\d{4}(\d{4}), format$1****$2}转为 JSON 格式显示如下 输出 YAML 文件中的全部数据 {Y3800: {phone: {rule: (\\d{3})\\d{4}(\\d{4}),format: $1****$2},idCard: {rule: (?\\w{3})\\w(?\\w{4}),format: *}},Y3801: {name: {rule: .(?.),format: },idCard: {rule: (?\\w{3})\\w(?\\w{4}),format: },list: {card: {rule: \\d(?\\d{4}),format: }}}
}输出 Y3800 层级下的数据 {phone: {rule: (\\d{3})\\d{4}(\\d{4}),format: $1****$2},idCard: {rule: (?\\w{3})\\w(?\\w{4}),format: *}
}输出 phone 层级下的数据 {rule: (\\d{3})\\d{4}(\\d{4}),format: $1****$2
}在这里我们需要仔细思考一下在我们通过 Key 获取指定层级下的数据时我们需要不断的调用 Map.get(Key) 方法即结构每嵌套一次就需要一次 getKey那么这里是否有优化的方法呢
答案是有的因为有问题就会有答案。
优化后
首先我们需要先了解一个概念
Y3800:phone:rule: (\\d{3})\\d{4}(\\d{4})format: $1****$2当我们要从上述数据中获取 phone 的脱敏规则时我们需要先从 Map 中 get(Y3800) 获取 Y3800 下的数据再通过 get(phone) 获取 phone 下的规则那么 Y3800-phone 就是 phone 的键路径。
基于此我们可以实现这样一个方法我们直接给出指定字段的键路径在方法中通过递归的方式从 Map 中获取到该键路径下的所有数据然后返回即可。
即优化思路为通过递归和判断来遍历嵌套的 Map直到找到键路径所对应的最里层的嵌套 Map并返回该 Map 对象。
优化后方法如下
/*** 递归获取嵌套 Map 数据** param map 嵌套数据源的 Map* param keys 嵌套键路径* return 嵌套数据对应的 Map*/
SuppressWarnings(unchecked)
public static MapString, Object getNestedMapValues(MapString, Object map, String... keys) {// 如果键路径为空或者第一个键不在 Map 中则返回 nullif (keys.length 0 || !map.containsKey(keys[0])) {return null;}// 获取第一个键对应的嵌套对象Object nestedObject map.get(keys[0]);// 如果键路径长度为 1说明已经到达最里层的嵌套 Map直接返回该 Map 对象if (keys.length 1) {if (nestedObject instanceof Map) {return (MapString, Object) nestedObject;} else {return null;}} else {// 如果嵌套对象是 Map继续递归查找下一个键的嵌套 Mapif (nestedObject instanceof Map) {return getNestedMapValues((MapString, Object) nestedObject, Arrays.copyOfRange(keys, 1, keys.length));} else {// 嵌套对象既不是 Map 也不是 List返回 nullreturn null;}}
}调用方法时传入 Key 的嵌套路径即可
public static void main(String[] args) {// 加载 YAML 文件并获取顶层的 Map 对象MapString, Object yamlMap loadYaml(/desensitize.yml);System.out.println(yamlMap);// 获取 Y3800 - phone 下的数据转为 MapMapString, Object y3800PhoneMap YamlUtils.getNestedMap(yamlMap, Y3800, phone);System.out.println(Y3800 - phone : y3800NameMap);
}具体来说主要分为以下几步
首先判断键路径是否为空或者第一个键是否在 Map 中。如果键路径为空或者第一个键不在 Map 中则返回 null。获取第一个键对应的嵌套对象。通过 get 方法获取第一个键对应的嵌套对象。判断是否到达最里层的嵌套 Map。如果键路径长度为 1说明已经到达最里层的嵌套 Map直接返回该 Map 对象。继续递归查找下一个键的嵌套 Map。如果嵌套对象是 Map则继续递归查找下一个键的嵌套 Map。返回结果。返回递归查找的结果。
对数据进行脱敏处理
获取到字段的脱敏规则后我们就可以编写方法实现对源数据做脱敏处理脱敏方法如下
/*** 使用指定规则对数据进行脱敏处理** param data 要进行脱敏处理的数据* param map 包含脱敏规则和格式的参数映射* - rule 表示脱敏规则的正则表达式* - format 表示替换脱敏部分的字符串默认为 ** return 脱敏后的数据*/
private static String desensitizeLogic(String data, MapString, Object map) {if (map.containsKey(rule)) {String rule (String) map.get(rule);String sign *;if (map.containsKey(format)) {sign (String) map.get(format);}return data.replaceAll(rule, sign);}return data;
}递归生成字段对应的键路径
目前我们已经实现了通过字段的键路径获取到该字段对应规则的方法 getNestedMapValues()那么接下来我们只需要生成字段对应的键路径然后调用方法 getNestedMapValues() 获取到脱敏规则后调用 desensitizeLogic() 对源数据进行脱敏即可。
提供源数据格式如下
{txEntity: {idCard: 130428197001180384,name: 赵士杰,list: [{phone: 17631007015},{phone: 17631007015}]},txHeader: {servNo: Y3801}
}根据上述数据结构首先我们需要从 txHeader 中获取 servNo之后递归遍历 txEntity 中的元素即可。
具体方法如下
/*** 对指定实体数据进行脱敏处理** param entity 要进行脱敏处理的实体数据* param servNo 当前交易的服务号用于记录日志* param path 当前实体数据在整个数据结构中的路径用于记录日志*/
public static void parseData(Object entity, String servNo, String path) {if (entity instanceof Map) {for (Map.EntryString, Object entry : ((MapString, Object) entity).entrySet()) {// 计算当前键值对在整个数据结构中的路径String currentPath path.isEmpty() ? entry.getKey() : path , entry.getKey();if (entry.getValue() instanceof Map) {// 如果当前值是 Map 类型则递归处理子节点parseData(entry.getValue(), servNo, currentPath);} else if (entry.getValue() instanceof List) {// 如果当前值是 List 类型则遍历列表中的每个元素并递归处理子节点for (Object item : (List) entry.getValue()) {if (item instanceof Map) {parseData(item, servNo, currentPath);}}} else {// 如果当前值不是 Map 或 List则进行脱敏处理String p servNo , currentPath;String[] keyPaths p.split(,);// 获取当前节点的脱敏规则和格式MapString, Object nestedMap getNestedMap(keyPaths);if(Objects.nonNull(nestedMap)){// 记录日志log.info(-----------------交易【{}】字段【{}】开始脱敏-----------------,servNo,currentPath.replace(,,-));log.info(原始值【{}:{}】,entry.getKey(),entry.getValue());log.info(脱敏规则:{},nestedMap);// 对当前节点的值进行脱敏处理String desensitized desensitizeLogic((String) entry.getValue(), nestedMap);entry.setValue(desensitized);// 记录日志log.info(脱敏值【{}:{}】,entry.getKey(),entry.getValue());log.info(-----------------交易【{}】字段【{}】脱敏结束-----------------,servNo,currentPath.replace(,,-));}}}}
}该方法接收一个实体数据 entity一个服务号 servNo 和一个路径 path 作为参数。在方法体内会遍历实体数据的键值对并根据具体情况递归处理子节点或进行脱敏处理。
当实体数据的值为 Map 类型时方法会递归处理子节点当值为 List 类型时方法会遍历列表中的每个元素并递归处理子节点当值既不是 Map 也不是 List 时方法会根据服务号和路径获取脱敏规则并对当前节点的值进行脱敏处理并记录脱敏日志。
脱敏处理的具体逻辑和规则通过调用 getNestedMap 方法和 desensitizeLogic 方法来实现其中 getNestedMap 方法用于获取脱敏规则desensitizeLogic 方法用于根据脱敏规则对数据进行脱敏处理。
注请注意本文中提供的数据样例的层次结构是和 YAML 中定义的结构是一样的再通过上述方法递归后生成的键路径是和从 YAML 中获取规则所需的键路径是一致的因此可以直接调用 getNestedMapValues() 获取脱敏规则。在实际使用中其他数据结构需要重写该逻辑。
脱敏测试
编写 Main 方法调用
public class Demo {public static MapString, Object getData() {HashMapString, Object phone new HashMap();phone.put(phone, 17631007015);HashMapString, Object phone2 new HashMap();phone2.put(phone, 17631007015);ListHashMapString, Object list new ArrayList();list.add(phone);list.add(phone2);HashMapString, Object txEntity new HashMap();txEntity.put(name, 赵士杰);txEntity.put(idCard, 130428197001180384);txEntity.put(list, list);HashMapString, Object result new HashMap();result.put(txEntity, txEntity);HashMapString, Object txHeader new HashMap();txHeader.put(servNo, Y3801);result.put(txHeader, txHeader);return result;}public static void main(String[] args) {MapString, Object data getData();// 假设data中包含接口返回的数据if (data.containsKey(txHeader) data.get(txHeader) instanceof Map) {String servNo ((MapString, String) data.get(txHeader)).get(servNo);DataDesensitizationUtils.parseData(data.get(txEntity), servNo, );}}}运行测试控制台输出如下
-----------------交易【Y3801】字段【idCard】开始脱敏-----------------
原始值【idCard:130428197001180384】
脱敏规则:{rule(?\w{3})\w(?\w{4}), format}
脱敏值【idCard:1300384】
-----------------交易【Y3801】字段【idCard】脱敏结束-----------------
-----------------交易【Y3801】字段【list-phone】开始脱敏-----------------
原始值【phone:17631007015】
脱敏规则:{rule(\d{3})\d{4}(\d{4}), format$1$2}
脱敏值【phone:1767015】
-----------------交易【Y3801】字段【list-phone】脱敏结束-----------------
-----------------交易【Y3801】字段【list-phone】开始脱敏-----------------
原始值【phone:17631007015】
脱敏规则:{rule(\d{3})\d{4}(\d{4}), format$1$2}
脱敏值【phone:1767015】
-----------------交易【Y3801】字段【list-phone】脱敏结束-----------------数据脱敏后如下
{txEntity: {idCard: 1300384,name: 赵士杰,list: [{phone: 1767015},{phone: 1767015}]},txHeader: {servNo: Y3801}
}完整工具类
封装成完整的工具类如下
/*** ClassName DataDesensitizationUtils* Description 数据脱敏工具类* Author 赵士杰* Date 2024/1/25 20:15*/
Slf4j
SuppressWarnings(unchecked)
public class DataDesensitizationUtils {// YAML 文件路径private static final String YAML_FILE_PATH /tuomin.yml;// 存储解析后的 YAML 数据private static MapString, Object map;static {// 创建 Yaml 对象Yaml yaml new Yaml();// 通过 getResourceAsStream 获取 YAML 文件的输入流try (InputStream in DataDesensitizationUtils.class.getResourceAsStream(YAML_FILE_PATH)) {// 解析 YAML 文件为 Map 对象map yaml.loadAs(in, Map.class);} catch (Exception e) {e.printStackTrace();}}/*** 获取嵌套的 Map 数据** param keys 嵌套键路径* return 嵌套数据对应的 Map*/private static MapString, Object getNestedMap(String... keys) {return getNestedMapValues(map, keys);}/*** 递归获取嵌套 Map 数据** param map 嵌套数据源的 Map* param keys 嵌套键路径* return 嵌套数据对应的 Map*/private static MapString, Object getNestedMapValues(MapString, Object map, String... keys) {// 如果键路径为空或者第一个键不在 Map 中则返回 nullif (keys.length 0 || !map.containsKey(keys[0])) {return null;}// 获取第一个键对应的嵌套对象Object nestedObject map.get(keys[0]);// 如果键路径长度为 1说明已经到达最里层的嵌套 Map直接返回该 Map 对象if (keys.length 1) {if (nestedObject instanceof Map) {return (MapString, Object) nestedObject;} else {return null;}} else {// 如果嵌套对象是 Map继续递归查找下一个键的嵌套 Mapif (nestedObject instanceof Map) {return getNestedMapValues((MapString, Object) nestedObject, Arrays.copyOfRange(keys, 1, keys.length));} else {// 嵌套对象既不是 Map 也不是 List返回 nullreturn null;}}}/*** 对指定实体数据进行脱敏处理** param entity 要进行脱敏处理的实体数据* param servNo 当前交易的服务号用于记录日志* param path 当前实体数据在整个数据结构中的路径用于记录日志*/public static void parseData(Object entity, String servNo, String path) {if (entity instanceof Map) {for (Map.EntryString, Object entry : ((MapString, Object) entity).entrySet()) {String currentPath path.isEmpty() ? entry.getKey() : path , entry.getKey();if (entry.getValue() instanceof Map) {parseData(entry.getValue(), servNo, currentPath);} else if (entry.getValue() instanceof List) {for (Object item : (List) entry.getValue()) {if (item instanceof Map) {parseData(item, servNo, currentPath);}}} else {String p servNo , currentPath;String[] keyPaths p.split(,);MapString, Object nestedMap getNestedMap(keyPaths);if (Objects.nonNull(nestedMap)) {log.info(-----------------交易【{}】字段【{}】开始脱敏-----------------, servNo, currentPath.replace(,, -));log.info(原始值【{}:{}】, entry.getKey(), entry.getValue());log.info(脱敏规则:{}, nestedMap);String desensitized desensitizeLogic((String) entry.getValue(), nestedMap);entry.setValue(desensitized);log.info(脱敏值【{}:{}】, entry.getKey(), entry.getValue());log.info(-----------------交易【{}】字段【{}】脱敏结束-----------------, servNo, currentPath.replace(,, -));}}}}}/*** 脱敏逻辑* param data 源数据* param map 脱敏规则* return 脱敏后的数据*/private static String desensitizeLogic(String data, MapString, Object map) {if (map.containsKey(rule)) {String rule (String) map.get(rule);String sign *;if (map.containsKey(format)) {sign (String) map.get(format);}return data.replaceAll(rule, sign);}return data;}}