From c64e48e0371b5c7911f32336a78b1c8923486d17 Mon Sep 17 00:00:00 2001 From: Looly Date: Mon, 9 Jun 2025 17:21:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=9C=AC=E5=8F=8A=E5=AF=8C=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=E8=A7=84?= =?UTF-8?q?=E5=88=99=E8=84=B1=E6=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hutool/core/data/RichTextMaskingUtil.java | 270 +++++----- .../masking/RichTextMaskingProcessor.java | 482 +++++++++--------- .../data/masking/RichTextMaskingRule.java | 465 ++++++++++------- .../core/data/RichTextMaskingUtilTest.java | 298 +++++------ 4 files changed, 810 insertions(+), 705 deletions(-) diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/RichTextMaskingUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/RichTextMaskingUtil.java index d776e6f32..af32444bb 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/data/RichTextMaskingUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/RichTextMaskingUtil.java @@ -10,153 +10,153 @@ import org.dromara.hutool.core.data.masking.RichTextMaskingRule; */ public class RichTextMaskingUtil { - /** - * 默认的富文本脱敏处理器 - */ - private static final RichTextMaskingProcessor DEFAULT_PROCESSOR = createDefaultProcessor(); + /** + * 默认的富文本脱敏处理器 + */ + private static final RichTextMaskingProcessor DEFAULT_PROCESSOR = createDefaultProcessor(); - /** - * 创建默认的富文本脱敏处理器 - * - * @return 默认的富文本脱敏处理器 - */ - private static RichTextMaskingProcessor createDefaultProcessor() { - RichTextMaskingProcessor processor = new RichTextMaskingProcessor(true); + /** + * 创建默认的富文本脱敏处理器 + * + * @return 默认的富文本脱敏处理器 + */ + private static RichTextMaskingProcessor createDefaultProcessor() { + final RichTextMaskingProcessor processor = new RichTextMaskingProcessor(true); - // 添加一些常用的脱敏规则 + // 添加一些常用的脱敏规则 - // 邮箱脱敏规则 - processor.addRule(new RichTextMaskingRule( - "邮箱", - "[\\w.-]+@[\\w.-]+\\.\\w+", - RichTextMaskingRule.MaskType.PARTIAL, - "[邮箱已隐藏]") - .setPreserveLeft(1) - .setPreserveRight(0) - .setMaskChar('*')); + // 邮箱脱敏规则 + processor.addRule(new RichTextMaskingRule( + "邮箱", + "[\\w.-]+@[\\w.-]+\\.\\w+", + RichTextMaskingRule.MaskType.PARTIAL, + "[邮箱已隐藏]") + .setPreserveLeft(1) + .setPreserveRight(0) + .setMaskChar('*')); - // 网址脱敏规则 - processor.addRule(new RichTextMaskingRule( - "网址", - "https?://[\\w.-]+(?:/[\\w.-]*)*", - RichTextMaskingRule.MaskType.REPLACE, - "[网址已隐藏]")); + // 网址脱敏规则 + processor.addRule(new RichTextMaskingRule( + "网址", + "https?://[\\w.-]+(?:/[\\w.-]*)*", + RichTextMaskingRule.MaskType.REPLACE, + "[网址已隐藏]")); - // 敏感词脱敏规则(示例) - processor.addRule(new RichTextMaskingRule( - "敏感词", - "(机密|绝密|内部资料|秘密|保密)", - RichTextMaskingRule.MaskType.FULL, - "***") - .setMaskChar('*')); + // 敏感词脱敏规则(示例) + processor.addRule(new RichTextMaskingRule( + "敏感词", + "(机密|绝密|内部资料|秘密|保密)", + RichTextMaskingRule.MaskType.FULL, + "***") + .setMaskChar('*')); - return processor; - } + return processor; + } - /** - * 对富文本内容进行脱敏处理 - * - * @param text 富文本内容 - * @return 脱敏后的文本 - */ - public static String mask(String text) { - return DEFAULT_PROCESSOR.mask(text); - } + /** + * 对富文本内容进行脱敏处理 + * + * @param text 富文本内容 + * @return 脱敏后的文本 + */ + public static String mask(final String text) { + return DEFAULT_PROCESSOR.mask(text); + } - /** - * 使用自定义处理器对富文本内容进行脱敏处理 - * - * @param text 富文本内容 - * @param processor 自定义处理器 - * @return 脱敏后的文本 - */ - public static String mask(String text, RichTextMaskingProcessor processor) { - return processor.mask(text); - } + /** + * 使用自定义处理器对富文本内容进行脱敏处理 + * + * @param text 富文本内容 + * @param processor 自定义处理器 + * @return 脱敏后的文本 + */ + public static String mask(final String text, final RichTextMaskingProcessor processor) { + return processor.mask(text); + } - /** - * 创建一个新的富文本脱敏处理器 - * - * @param preserveHtmlTags 是否保留HTML标签 - * @return 富文本脱敏处理器 - */ - public static RichTextMaskingProcessor createProcessor(boolean preserveHtmlTags) { - return new RichTextMaskingProcessor(preserveHtmlTags); - } + /** + * 创建一个新的富文本脱敏处理器 + * + * @param preserveHtmlTags 是否保留HTML标签 + * @return 富文本脱敏处理器 + */ + public static RichTextMaskingProcessor createProcessor(final boolean preserveHtmlTags) { + return new RichTextMaskingProcessor(preserveHtmlTags); + } - /** - * 创建一个邮箱脱敏规则 - * - * @return 邮箱脱敏规则 - */ - public static RichTextMaskingRule createEmailRule() { - return new RichTextMaskingRule( - "邮箱", - "[\\w.-]+@[\\w.-]+\\.\\w+", - RichTextMaskingRule.MaskType.PARTIAL, - null) - .setPreserveLeft(1) - .setPreserveRight(0) - .setMaskChar('*'); - } + /** + * 创建一个邮箱脱敏规则 + * + * @return 邮箱脱敏规则 + */ + public static RichTextMaskingRule createEmailRule() { + return new RichTextMaskingRule( + "邮箱", + "[\\w.-]+@[\\w.-]+\\.\\w+", + RichTextMaskingRule.MaskType.PARTIAL, + null) + .setPreserveLeft(1) + .setPreserveRight(0) + .setMaskChar('*'); + } - /** - * 创建一个网址脱敏规则 - * - * @param replacement 替换文本 - * @return 网址脱敏规则 - */ - public static RichTextMaskingRule createUrlRule(String replacement) { - return new RichTextMaskingRule( - "网址", - "https?://[\\w.-]+(?:/[\\w.-]*)*", - RichTextMaskingRule.MaskType.REPLACE, - replacement); - } + /** + * 创建一个网址脱敏规则 + * + * @param replacement 替换文本 + * @return 网址脱敏规则 + */ + public static RichTextMaskingRule createUrlRule(final String replacement) { + return new RichTextMaskingRule( + "网址", + "https?://[\\w.-]+(?:/[\\w.-]*)*", + RichTextMaskingRule.MaskType.REPLACE, + replacement); + } - /** - * 创建一个敏感词脱敏规则 - * - * @param pattern 敏感词正则表达式 - * @return 敏感词脱敏规则 - */ - public static RichTextMaskingRule createSensitiveWordRule(String pattern) { - return new RichTextMaskingRule( - "敏感词", - pattern, - RichTextMaskingRule.MaskType.FULL, - null) - .setMaskChar('*'); - } + /** + * 创建一个敏感词脱敏规则 + * + * @param pattern 敏感词正则表达式 + * @return 敏感词脱敏规则 + */ + public static RichTextMaskingRule createSensitiveWordRule(final String pattern) { + return new RichTextMaskingRule( + "敏感词", + pattern, + RichTextMaskingRule.MaskType.FULL, + null) + .setMaskChar('*'); + } - /** - * 创建一个自定义脱敏规则 - * - * @param name 规则名称 - * @param pattern 匹配模式(正则表达式) - * @param maskType 脱敏类型 - * @param replacement 替换内容 - * @return 自定义脱敏规则 - */ - public static RichTextMaskingRule createCustomRule(String name, String pattern, - RichTextMaskingRule.MaskType maskType, - String replacement) { - return new RichTextMaskingRule(name, pattern, maskType, replacement); - } + /** + * 创建一个自定义脱敏规则 + * + * @param name 规则名称 + * @param pattern 匹配模式(正则表达式) + * @param maskType 脱敏类型 + * @param replacement 替换内容 + * @return 自定义脱敏规则 + */ + public static RichTextMaskingRule createCustomRule(final String name, final String pattern, + final RichTextMaskingRule.MaskType maskType, + final String replacement) { + return new RichTextMaskingRule(name, pattern, maskType, replacement); + } - /** - * 创建一个部分脱敏规则 - * - * @param name 规则名称 - * @param pattern 匹配模式(正则表达式) - * @param preserveLeft 保留左侧字符数 - * @param preserveRight 保留右侧字符数 - * @param maskChar 脱敏字符 - * @return 部分脱敏规则 - */ - public static RichTextMaskingRule createPartialMaskRule(String name, String pattern, - int preserveLeft, int preserveRight, - char maskChar) { - return new RichTextMaskingRule(name, pattern, preserveLeft, preserveRight, maskChar); - } + /** + * 创建一个部分脱敏规则 + * + * @param name 规则名称 + * @param pattern 匹配模式(正则表达式) + * @param preserveLeft 保留左侧字符数 + * @param preserveRight 保留右侧字符数 + * @param maskChar 脱敏字符 + * @return 部分脱敏规则 + */ + public static RichTextMaskingRule createPartialMaskRule(final String name, final String pattern, + final int preserveLeft, final int preserveRight, + final char maskChar) { + return new RichTextMaskingRule(name, pattern, preserveLeft, preserveRight, maskChar); + } } diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/RichTextMaskingProcessor.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/RichTextMaskingProcessor.java index afdd0f016..91a22e8a4 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/RichTextMaskingProcessor.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/RichTextMaskingProcessor.java @@ -14,299 +14,283 @@ import java.util.regex.Pattern; */ public class RichTextMaskingProcessor { - /** - * 脱敏规则列表 - */ - private final List rules = new ArrayList<>(); + /** + * 脱敏规则列表 + */ + private final List rules = new ArrayList<>(); - /** - * 是否保留HTML标签 - */ - private boolean preserveHtmlTags = true; + /** + * 是否保留HTML标签 + */ + private boolean preserveHtmlTags = true; - /** - * 默认的脱敏字符 - */ - private char defaultMaskChar = '*'; + /** + * 构造函数 + */ + public RichTextMaskingProcessor() { + } - /** - * 构造函数 - */ - public RichTextMaskingProcessor() { - } + /** + * 构造函数 + * + * @param preserveHtmlTags 是否保留HTML标签 + */ + public RichTextMaskingProcessor(final boolean preserveHtmlTags) { + this.preserveHtmlTags = preserveHtmlTags; + } - /** - * 构造函数 - * - * @param preserveHtmlTags 是否保留HTML标签 - */ - public RichTextMaskingProcessor(boolean preserveHtmlTags) { - this.preserveHtmlTags = preserveHtmlTags; - } + /** + * 添加脱敏规则 + * + * @param rule 脱敏规则 + * @return this + */ + public RichTextMaskingProcessor addRule(final RichTextMaskingRule rule) { + this.rules.add(rule); + return this; + } - /** - * 添加脱敏规则 - * - * @param rule 脱敏规则 - * @return this - */ - public RichTextMaskingProcessor addRule(RichTextMaskingRule rule) { - this.rules.add(rule); - return this; - } + /** + * 对文本内容进行脱敏处理 + * + * @param text 文本内容 + * @return 脱敏后的文本 + */ + public String mask(final String text) { + if (StrUtil.isBlank(text)) { + return text; + } - /** - * 设置默认的脱敏字符 - * - * @param defaultMaskChar 默认的脱敏字符 - * @return this - */ - public RichTextMaskingProcessor setDefaultMaskChar(char defaultMaskChar) { - this.defaultMaskChar = defaultMaskChar; - return this; - } + // 如果是HTML内容,则需要特殊处理 + if (preserveHtmlTags && isHtmlContent(text)) { + return maskHtmlContent(text); + } else { + // 普通文本直接处理 + return maskPlainText(text); + } + } - /** - * 对文本内容进行脱敏处理 - * - * @param text 文本内容 - * @return 脱敏后的文本 - */ - public String mask(String text) { - if (StrUtil.isBlank(text)) { - return text; - } + /** + * 判断是否为HTML内容 + * + * @param text 文本内容 + * @return 是否为HTML内容 + */ + private boolean isHtmlContent(final String text) { + // 简单判断是否包含HTML标签 + return text.contains("<") && text.contains(">") && + (text.contains("")); + } - // 如果是HTML内容,则需要特殊处理 - if (preserveHtmlTags && isHtmlContent(text)) { - return maskHtmlContent(text); - } else { - // 普通文本直接处理 - return maskPlainText(text); - } - } + /** + * 对HTML内容进行脱敏处理 + * + * @param html HTML内容 + * @return 脱敏后的HTML + */ + private String maskHtmlContent(final String html) { + final StringBuilder result = new StringBuilder(); + int lastIndex = 0; + boolean inTag = false; + String currentTag = null; - /** - * 判断是否为HTML内容 - * - * @param text 文本内容 - * @return 是否为HTML内容 - */ - private boolean isHtmlContent(String text) { - // 简单判断是否包含HTML标签 - return text.contains("<") && text.contains(">") && - (text.contains("")); - } + for (int i = 0; i < html.length(); i++) { + final char c = html.charAt(i); - /** - * 对HTML内容进行脱敏处理 - * - * @param html HTML内容 - * @return 脱敏后的HTML - */ - private String maskHtmlContent(String html) { - StringBuilder result = new StringBuilder(); - int lastIndex = 0; - boolean inTag = false; - String currentTag = null; + if (c == '<') { + // 处理标签前的文本内容 + if (!inTag && i > lastIndex) { + final String textContent = html.substring(lastIndex, i); + result.append(processTextContentWithContext(textContent, currentTag)); + } - for (int i = 0; i < html.length(); i++) { - char c = html.charAt(i); + inTag = true; + lastIndex = i; - if (c == '<') { - // 处理标签前的文本内容 - if (!inTag && i > lastIndex) { - String textContent = html.substring(lastIndex, i); - result.append(processTextContentWithContext(textContent, currentTag)); - } + // 尝试获取当前标签名 + int tagNameStart = i + 1; + if (tagNameStart < html.length()) { + // 跳过结束标签的斜杠 + if (html.charAt(tagNameStart) == '/') { + tagNameStart++; + } - inTag = true; - lastIndex = i; + // 查找标签名结束位置 + int tagNameEnd = html.indexOf(' ', tagNameStart); + if (tagNameEnd == -1) { + tagNameEnd = html.indexOf('>', tagNameStart); + } - // 尝试获取当前标签名 - int tagNameStart = i + 1; - if (tagNameStart < html.length()) { - // 跳过结束标签的斜杠 - if (html.charAt(tagNameStart) == '/') { - tagNameStart++; - } + if (tagNameEnd > tagNameStart) { + currentTag = html.substring(tagNameStart, tagNameEnd).toLowerCase(); + } + } + } else if (c == '>' && inTag) { + inTag = false; + result.append(html, lastIndex, i + 1); // 保留标签 + lastIndex = i + 1; + } + } - // 查找标签名结束位置 - int tagNameEnd = html.indexOf(' ', tagNameStart); - if (tagNameEnd == -1) { - tagNameEnd = html.indexOf('>', tagNameStart); - } + // 处理最后一部分 + if (lastIndex < html.length()) { + if (inTag) { + // 如果还在标签内,直接添加剩余部分 + result.append(html.substring(lastIndex)); + } else { + // 处理最后的文本内容 + final String textContent = html.substring(lastIndex); + result.append(processTextContentWithContext(textContent, currentTag)); + } + } - if (tagNameEnd > tagNameStart) { - currentTag = html.substring(tagNameStart, tagNameEnd).toLowerCase(); - } - } - } else if (c == '>' && inTag) { - inTag = false; - result.append(html, lastIndex, i + 1); // 保留标签 - lastIndex = i + 1; - } - } + return result.toString(); + } - // 处理最后一部分 - if (lastIndex < html.length()) { - if (inTag) { - // 如果还在标签内,直接添加剩余部分 - result.append(html.substring(lastIndex)); - } else { - // 处理最后的文本内容 - String textContent = html.substring(lastIndex); - result.append(processTextContentWithContext(textContent, currentTag)); - } - } + /** + * 根据上下文处理文本内容 + * + * @param text 文本内容 + * @param tagName 当前所在的标签名 + * @return 处理后的文本 + */ + private String processTextContentWithContext(final String text, final String tagName) { + if (StrUtil.isBlank(text)) { + return text; + } - return result.toString(); - } + String result = text; - /** - * 根据上下文处理文本内容 - * - * @param text 文本内容 - * @param tagName 当前所在的标签名 - * @return 处理后的文本 - */ - private String processTextContentWithContext(String text, String tagName) { - if (StrUtil.isBlank(text)) { - return text; - } + for (final RichTextMaskingRule rule : rules) { + // 检查是否需要根据标签进行过滤 + if (tagName != null) { + // 如果设置了只包含特定标签且当前标签不在列表中,则跳过 + if (!rule.getIncludeTags().isEmpty() && !rule.getIncludeTags().contains(tagName)) { + continue; + } - String result = text; + // 如果当前标签在排除列表中,则跳过 + if (rule.getExcludeTags().contains(tagName)) { + continue; + } + } - for (RichTextMaskingRule rule : rules) { - // 检查是否需要根据标签进行过滤 - if (tagName != null) { - // 如果设置了只包含特定标签且当前标签不在列表中,则跳过 - if (!rule.getIncludeTags().isEmpty() && !rule.getIncludeTags().contains(tagName)) { - continue; - } + // 应用脱敏规则 + result = applyMaskingRule(result, rule); + } - // 如果当前标签在排除列表中,则跳过 - if (rule.getExcludeTags().contains(tagName)) { - continue; - } - } + return result; + } - // 应用脱敏规则 - result = applyMaskingRule(result, rule); - } + /** + * 对普通文本进行脱敏处理 + * + * @param text 文本内容 + * @return 脱敏后的文本 + */ + private String maskPlainText(final String text) { + String result = text; - return result; - } + for (final RichTextMaskingRule rule : rules) { + result = applyMaskingRule(result, rule); + } - /** - * 对普通文本进行脱敏处理 - * - * @param text 文本内容 - * @return 脱敏后的文本 - */ - private String maskPlainText(String text) { - String result = text; + return result; + } - for (RichTextMaskingRule rule : rules) { - result = applyMaskingRule(result, rule); - } + /** + * 应用脱敏规则 + * + * @param text 文本内容 + * @param rule 脱敏规则 + * @return 脱敏后的文本 + */ + private String applyMaskingRule(final String text, final RichTextMaskingRule rule) { + if (StrUtil.isBlank(text) || StrUtil.isBlank(rule.getPattern())) { + return text; + } - return result; - } + final Pattern pattern = Pattern.compile(rule.getPattern()); + final Matcher matcher = pattern.matcher(text); - /** - * 应用脱敏规则 - * - * @param text 文本内容 - * @param rule 脱敏规则 - * @return 脱敏后的文本 - */ - private String applyMaskingRule(String text, RichTextMaskingRule rule) { - if (StrUtil.isBlank(text) || StrUtil.isBlank(rule.getPattern())) { - return text; - } + final StringBuffer sb = new StringBuffer(); - Pattern pattern = Pattern.compile(rule.getPattern()); - Matcher matcher = pattern.matcher(text); + while (matcher.find()) { + final String matched = matcher.group(); + final String replacement; - StringBuffer sb = new StringBuffer(); + switch (rule.getMaskType()) { + case FULL: + // 完全脱敏,用脱敏字符替换整个匹配内容 + replacement = StrUtil.repeat(rule.getMaskChar(), matched.length()); + break; - while (matcher.find()) { - String matched = matcher.group(); - String replacement; + case PARTIAL: + // 部分脱敏,保留部分原始内容 + replacement = partialMask(matched, rule.getPreserveLeft(), rule.getPreserveRight(), rule.getMaskChar()); + break; - switch (rule.getMaskType()) { - case FULL: - // 完全脱敏,用脱敏字符替换整个匹配内容 - replacement = StrUtil.repeat(rule.getMaskChar(), matched.length()); - break; + case REPLACE: + // 替换脱敏,用指定文本替换 + replacement = rule.getReplacement(); + break; - case PARTIAL: - // 部分脱敏,保留部分原始内容 - replacement = partialMask(matched, rule.getPreserveLeft(), rule.getPreserveRight(), rule.getMaskChar()); - break; + default: + replacement = matched; + break; + } - case REPLACE: - // 替换脱敏,用指定文本替换 - replacement = rule.getReplacement(); - break; + // 处理正则表达式中的特殊字符 + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + } - default: - replacement = matched; - break; - } + matcher.appendTail(sb); - // 处理正则表达式中的特殊字符 - matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); - } + return sb.toString(); + } - matcher.appendTail(sb); + /** + * 部分脱敏,保留部分原始内容 + * + * @param text 原文本 + * @param preserveLeft 保留左侧字符数 + * @param preserveRight 保留右侧字符数 + * @param maskChar 脱敏字符 + * @return 脱敏后的文本 + */ + private String partialMask(final String text, int preserveLeft, int preserveRight, final char maskChar) { + if (StrUtil.isBlank(text)) { + return text; + } - return sb.toString(); - } + final int length = text.length(); - /** - * 部分脱敏,保留部分原始内容 - * - * @param text 原文本 - * @param preserveLeft 保留左侧字符数 - * @param preserveRight 保留右侧字符数 - * @param maskChar 脱敏字符 - * @return 脱敏后的文本 - */ - private String partialMask(String text, int preserveLeft, int preserveRight, char maskChar) { - if (StrUtil.isBlank(text)) { - return text; - } + // 调整保留字符数,确保不超过文本长度 + preserveLeft = Math.min(preserveLeft, length); + preserveRight = Math.min(preserveRight, length - preserveLeft); - int length = text.length(); + // 计算需要脱敏的字符数 + final int maskLength = length - preserveLeft - preserveRight; - // 调整保留字符数,确保不超过文本长度 - preserveLeft = Math.min(preserveLeft, length); - preserveRight = Math.min(preserveRight, length - preserveLeft); + if (maskLength <= 0) { + return text; + } - // 计算需要脱敏的字符数 - int maskLength = length - preserveLeft - preserveRight; + final StringBuilder sb = new StringBuilder(length); - if (maskLength <= 0) { - return text; - } + // 添加左侧保留的字符 + if (preserveLeft > 0) { + sb.append(text, 0, preserveLeft); + } - StringBuilder sb = new StringBuilder(length); + // 添加脱敏字符 + sb.append(StrUtil.repeat(maskChar, maskLength)); - // 添加左侧保留的字符 - if (preserveLeft > 0) { - sb.append(text, 0, preserveLeft); - } + // 添加右侧保留的字符 + if (preserveRight > 0) { + sb.append(text, length - preserveRight, length); + } - // 添加脱敏字符 - sb.append(StrUtil.repeat(maskChar, maskLength)); - - // 添加右侧保留的字符 - if (preserveRight > 0) { - sb.append(text, length - preserveRight, length); - } - - return sb.toString(); - } + return sb.toString(); + } } diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/RichTextMaskingRule.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/RichTextMaskingRule.java index 2ec006efd..596d633fd 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/RichTextMaskingRule.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/RichTextMaskingRule.java @@ -10,214 +10,335 @@ import java.util.Set; */ public class RichTextMaskingRule { - /** - * 脱敏类型枚举 - */ - public enum MaskType { - /** - * 完全脱敏,将匹配的内容完全替换为指定字符 - */ - FULL, + /** + * 脱敏类型枚举 + */ + public enum MaskType { + /** + * 完全脱敏,将匹配的内容完全替换为指定字符 + */ + FULL, - /** - * 部分脱敏,保留部分原始内容 - */ - PARTIAL, + /** + * 部分脱敏,保留部分原始内容 + */ + PARTIAL, - /** - * 替换脱敏,将匹配的内容替换为指定的替换文本 - */ - REPLACE - } + /** + * 替换脱敏,将匹配的内容替换为指定的替换文本 + */ + REPLACE + } - /** - * 规则名称 - */ - private String name; + /** + * 规则名称 + */ + private String name; - /** - * 匹配模式(正则表达式) - */ - private String pattern; + /** + * 匹配模式(正则表达式) + */ + private String pattern; - /** - * 脱敏类型 - */ - private MaskType maskType; + /** + * 脱敏类型 + */ + private MaskType maskType; - /** - * 替换内容 - */ - private String replacement; + /** + * 替换内容 + */ + private String replacement; - /** - * 保留左侧字符数(用于PARTIAL类型) - */ - private int preserveLeft; + /** + * 保留左侧字符数(用于PARTIAL类型) + */ + private int preserveLeft; - /** - * 保留右侧字符数(用于PARTIAL类型) - */ - private int preserveRight; + /** + * 保留右侧字符数(用于PARTIAL类型) + */ + private int preserveRight; - /** - * 脱敏字符 - */ - private char maskChar = '*'; + /** + * 脱敏字符 + */ + private char maskChar = '*'; - /** - * 是否处理HTML标签内容 - */ - private boolean processHtmlTags = false; + /** + * 是否处理HTML标签内容 + */ + private boolean processHtmlTags = false; - /** - * 需要排除的HTML标签 - */ - private Set excludeTags = new HashSet<>(); + /** + * 需要排除的HTML标签 + */ + private Set excludeTags = new HashSet<>(); - /** - * 仅处理指定的HTML标签 - */ - private Set includeTags = new HashSet<>(); + /** + * 仅处理指定的HTML标签 + */ + private Set includeTags = new HashSet<>(); - /** - * 构造函数 - */ - public RichTextMaskingRule() { - } + /** + * 构造函数 + */ + public RichTextMaskingRule() { + } - /** - * 构造函数 - * - * @param name 规则名称 - * @param pattern 匹配模式(正则表达式) - * @param maskType 脱敏类型 - * @param replacement 替换内容 - */ - public RichTextMaskingRule(String name, String pattern, MaskType maskType, String replacement) { - this.name = name; - this.pattern = pattern; - this.maskType = maskType; - this.replacement = replacement; - } + /** + * 构造函数 + * + * @param name 规则名称 + * @param pattern 匹配模式(正则表达式) + * @param maskType 脱敏类型 + * @param replacement 替换内容 + */ + public RichTextMaskingRule(final String name, final String pattern, final MaskType maskType, final String replacement) { + this.name = name; + this.pattern = pattern; + this.maskType = maskType; + this.replacement = replacement; + } - /** - * 构造函数,用于部分脱敏 - * - * @param name 规则名称 - * @param pattern 匹配模式(正则表达式) - * @param preserveLeft 保留左侧字符数 - * @param preserveRight 保留右侧字符数 - * @param maskChar 脱敏字符 - */ - public RichTextMaskingRule(String name, String pattern, int preserveLeft, int preserveRight, char maskChar) { - this.name = name; - this.pattern = pattern; - this.maskType = MaskType.PARTIAL; - this.preserveLeft = preserveLeft; - this.preserveRight = preserveRight; - this.maskChar = maskChar; - } + /** + * 构造函数,用于部分脱敏 + * + * @param name 规则名称 + * @param pattern 匹配模式(正则表达式) + * @param preserveLeft 保留左侧字符数 + * @param preserveRight 保留右侧字符数 + * @param maskChar 脱敏字符 + */ + public RichTextMaskingRule(final String name, final String pattern, final int preserveLeft, final int preserveRight, final char maskChar) { + this.name = name; + this.pattern = pattern; + this.maskType = MaskType.PARTIAL; + this.preserveLeft = preserveLeft; + this.preserveRight = preserveRight; + this.maskChar = maskChar; + } - // Getter and Setter methods + // Getter and Setter methods - public String getName() { - return name; - } + /** + * 获取规则名称 + * + * @return 规则名称 + */ + public String getName() { + return name; + } - public RichTextMaskingRule setName(String name) { - this.name = name; - return this; - } + /** + * 设置规则名称 + * + * @param name 名称 + * @return this + */ + public RichTextMaskingRule setName(final String name) { + this.name = name; + return this; + } - public String getPattern() { - return pattern; - } + /** + * 获取匹配模式(正则表达式) + * @return 匹配模式(正则表达式) + */ + public String getPattern() { + return pattern; + } - public RichTextMaskingRule setPattern(String pattern) { - this.pattern = pattern; - return this; - } + /** + * 设置匹配模式(正则表达式) + * @param pattern 匹配模式(正则表达式) + * @return this + */ + public RichTextMaskingRule setPattern(final String pattern) { + this.pattern = pattern; + return this; + } - public MaskType getMaskType() { - return maskType; - } - public RichTextMaskingRule setMaskType(MaskType maskType) { - this.maskType = maskType; - return this; - } + /** + * 获取脱敏类型 + * + * @return 脱敏类型 + */ + public MaskType getMaskType() { + return maskType; + } - public String getReplacement() { - return replacement; - } + /** + * 设置脱敏类型 + * + * @param maskType 脱敏类型 + * @return this + */ + public RichTextMaskingRule setMaskType(final MaskType maskType) { + this.maskType = maskType; + return this; + } - public RichTextMaskingRule setReplacement(String replacement) { - this.replacement = replacement; - return this; - } + /** + * 获取替换内容 + * + * @return 替换内容 + */ + public String getReplacement() { + return replacement; + } - public int getPreserveLeft() { - return preserveLeft; - } + /** + * 设置替换内容 + * + * @param replacement 替换内容 + * @return this + */ + public RichTextMaskingRule setReplacement(final String replacement) { + this.replacement = replacement; + return this; + } - public RichTextMaskingRule setPreserveLeft(int preserveLeft) { - this.preserveLeft = preserveLeft; - return this; - } + /** + * 获取保留左侧字符数 + * + * @return 保留左侧字符数 + */ + public int getPreserveLeft() { + return preserveLeft; + } - public int getPreserveRight() { - return preserveRight; - } + /** + * 设置保留左侧字符数 + * + * @param preserveLeft 保留左侧字符数 + * @return this + */ + public RichTextMaskingRule setPreserveLeft(final int preserveLeft) { + this.preserveLeft = preserveLeft; + return this; + } - public RichTextMaskingRule setPreserveRight(int preserveRight) { - this.preserveRight = preserveRight; - return this; - } + /** + * 获取保留右侧字符数 + * + * @return 保留右侧字符数 + */ + public int getPreserveRight() { + return preserveRight; + } - public char getMaskChar() { - return maskChar; - } + /** + * 设置保留右侧字符数 + * + * @param preserveRight 保留右侧字符数 + * @return this + */ + public RichTextMaskingRule setPreserveRight(final int preserveRight) { + this.preserveRight = preserveRight; + return this; + } - public RichTextMaskingRule setMaskChar(char maskChar) { - this.maskChar = maskChar; - return this; - } + /** + * 获取脱敏字符 + * + * @return 脱敏字符 + */ + public char getMaskChar() { + return maskChar; + } - public boolean isProcessHtmlTags() { - return processHtmlTags; - } + /** + * 设置脱敏字符 + * + * @param maskChar 脱敏字符 + * @return this + */ + public RichTextMaskingRule setMaskChar(final char maskChar) { + this.maskChar = maskChar; + return this; + } - public RichTextMaskingRule setProcessHtmlTags(boolean processHtmlTags) { - this.processHtmlTags = processHtmlTags; - return this; - } + /** + * 获取是否处理HTML标签内容 + * + * @return 是否处理HTML标签内容 + */ + public boolean isProcessHtmlTags() { + return processHtmlTags; + } - public Set getExcludeTags() { - return excludeTags; - } + /** + * 设置是否处理HTML标签内容 + * + * @param processHtmlTags 是否处理HTML标签内容 + * @return this + */ + public RichTextMaskingRule setProcessHtmlTags(final boolean processHtmlTags) { + this.processHtmlTags = processHtmlTags; + return this; + } - public RichTextMaskingRule setExcludeTags(Set excludeTags) { - this.excludeTags = excludeTags; - return this; - } + /** + * 获取需要排除的HTML标签 + * + * @return 需要排除的HTML标签 + */ + public Set getExcludeTags() { + return excludeTags; + } - public RichTextMaskingRule addExcludeTag(String tag) { - this.excludeTags.add(tag.toLowerCase()); - return this; - } + /** + * 设置需要排除的HTML标签 + * + * @param excludeTags 需要排除的HTML标签 + * @return this + */ + public RichTextMaskingRule setExcludeTags(final Set excludeTags) { + this.excludeTags = excludeTags; + return this; + } - public Set getIncludeTags() { - return includeTags; - } + /** + * 添加需要排除的HTML标签 + * + * @param tag 需要排除的HTML标签 + * @return this + */ + public RichTextMaskingRule addExcludeTag(final String tag) { + this.excludeTags.add(tag.toLowerCase()); + return this; + } - public RichTextMaskingRule setIncludeTags(Set includeTags) { - this.includeTags = includeTags; - return this; - } + /** + * 获取仅处理指定的HTML标签 + * + * @return 仅处理指定的HTML标签 + */ + public Set getIncludeTags() { + return includeTags; + } - public RichTextMaskingRule addIncludeTag(String tag) { - this.includeTags.add(tag.toLowerCase()); - return this; - } + /** + * 设置仅处理指定的HTML标签 + * + * @param includeTags 仅处理指定的HTML标签 + * @return this + */ + public RichTextMaskingRule setIncludeTags(final Set includeTags) { + this.includeTags = includeTags; + return this; + } + + /** + * 添加仅处理指定的HTML标签 + * + * @param tag 仅处理指定的HTML标签 + * @return this + */ + public RichTextMaskingRule addIncludeTag(final String tag) { + this.includeTags.add(tag.toLowerCase()); + return this; + } } diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/data/RichTextMaskingUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/data/RichTextMaskingUtilTest.java index 7a08eb997..402c4944d 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/data/RichTextMaskingUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/data/RichTextMaskingUtilTest.java @@ -15,188 +15,188 @@ import java.util.Set; */ public class RichTextMaskingUtilTest { - @Test - public void testDefaultMask() { - // 测试默认脱敏功能 - String html = "这是一封邮件,联系人:test@example.com,网址:https://www.example.com,包含机密信息。"; - String masked = RichTextMaskingUtil.mask(html); + @Test + public void testDefaultMask() { + // 测试默认脱敏功能 + final String html = "这是一封邮件,联系人:test@example.com,网址:https://www.example.com,包含机密信息。"; + final String masked = RichTextMaskingUtil.mask(html); - // 验证邮箱被脱敏 - Assertions.assertFalse(masked.contains("test@example.com")); - Assertions.assertTrue(masked.contains("t***")); + // 验证邮箱被脱敏 + Assertions.assertFalse(masked.contains("test@example.com")); + Assertions.assertTrue(masked.contains("t***")); - // 验证网址被脱敏 - Assertions.assertFalse(masked.contains("https://www.example.com")); - Assertions.assertTrue(masked.contains("[网址已隐藏]")); + // 验证网址被脱敏 + Assertions.assertFalse(masked.contains("https://www.example.com")); + Assertions.assertTrue(masked.contains("[网址已隐藏]")); - // 验证敏感词被脱敏 - Assertions.assertFalse(masked.contains("机密")); - Assertions.assertTrue(masked.contains("**")); - } + // 验证敏感词被脱敏 + Assertions.assertFalse(masked.contains("机密")); + Assertions.assertTrue(masked.contains("**")); + } - @Test - public void testHtmlContentMask() { - // 测试HTML内容脱敏 - String html = "

这是一封邮件,联系人:test@example.com," + - "网址:https://www.example.com," + - "包含机密信息。

"; - String masked = RichTextMaskingUtil.mask(html); + @Test + public void testHtmlContentMask() { + // 测试HTML内容脱敏 + final String html = "

这是一封邮件,联系人:test@example.com," + + "网址:https://www.example.com," + + "包含机密信息。

"; + final String masked = RichTextMaskingUtil.mask(html); - // 验证HTML标签被保留 - Assertions.assertTrue(masked.contains("

")); - Assertions.assertTrue(masked.contains("

")); - Assertions.assertTrue(masked.contains("")); + // 验证HTML标签被保留 + Assertions.assertTrue(masked.contains("

")); + Assertions.assertTrue(masked.contains("

")); + Assertions.assertTrue(masked.contains("
")); - // 验证邮箱被脱敏 - Assertions.assertFalse(masked.contains("test@example.com")); - Assertions.assertTrue(masked.contains("t***")); + // 验证邮箱被脱敏 + Assertions.assertFalse(masked.contains("test@example.com")); + Assertions.assertTrue(masked.contains("t***")); - // 验证网址被脱敏 - Assertions.assertFalse(masked.contains("https://www.example.com")); - Assertions.assertTrue(masked.contains("[网址已隐藏]")); + // 验证网址被脱敏 + Assertions.assertFalse(masked.contains("https://www.example.com")); + Assertions.assertTrue(masked.contains("[网址已隐藏]")); - // 验证敏感词被脱敏 - Assertions.assertFalse(masked.contains("机密")); - Assertions.assertTrue(masked.contains("**")); - } + // 验证敏感词被脱敏 + Assertions.assertFalse(masked.contains("机密")); + Assertions.assertTrue(masked.contains("**")); + } - @Test - public void testCustomProcessor() { - // 创建自定义处理器 - RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); + @Test + public void testCustomProcessor() { + // 创建自定义处理器 + final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); - // 添加自定义规则 - 手机号码 - processor.addRule(RichTextMaskingUtil.createPartialMaskRule( - "手机号", - "1[3-9]\\d{9}", - 3, - 4, - '*')); + // 添加自定义规则 - 手机号码 + processor.addRule(RichTextMaskingUtil.createPartialMaskRule( + "手机号", + "1[3-9]\\d{9}", + 3, + 4, + '*')); - // 添加自定义规则 - 公司名称 - processor.addRule(RichTextMaskingUtil.createCustomRule( - "公司名称", - "XX科技有限公司", - RichTextMaskingRule.MaskType.REPLACE, - "[公司名称已隐藏]")); + // 添加自定义规则 - 公司名称 + processor.addRule(RichTextMaskingUtil.createCustomRule( + "公司名称", + "XX科技有限公司", + RichTextMaskingRule.MaskType.REPLACE, + "[公司名称已隐藏]")); - // 测试文本 - String text = "联系电话:13812345678,公司名称:XX科技有限公司"; - String masked = RichTextMaskingUtil.mask(text, processor); + // 测试文本 + final String text = "联系电话:13812345678,公司名称:XX科技有限公司"; + final String masked = RichTextMaskingUtil.mask(text, processor); - // 验证手机号被脱敏 - Assertions.assertFalse(masked.contains("13812345678")); - Assertions.assertTrue(masked.contains("138*****5678")); + // 验证手机号被脱敏 + Assertions.assertFalse(masked.contains("13812345678")); + Assertions.assertTrue(masked.contains("138****5678")); - // 验证公司名称被脱敏 - Assertions.assertFalse(masked.contains("XX科技有限公司")); - Assertions.assertTrue(masked.contains("[公司名称已隐藏]")); - } + // 验证公司名称被脱敏 + Assertions.assertFalse(masked.contains("XX科技有限公司")); + Assertions.assertTrue(masked.contains("[公司名称已隐藏]")); + } - @Test - public void testTagFiltering() { - // 创建自定义处理器 - RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); + @Test + public void testTagFiltering() { + // 创建自定义处理器 + final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); - // 创建只在特定标签中生效的规则 - RichTextMaskingRule rule = RichTextMaskingUtil.createCustomRule( - "标签内敏感信息", - "敏感信息", - RichTextMaskingRule.MaskType.REPLACE, - "[已隐藏]"); + // 创建只在特定标签中生效的规则 + final RichTextMaskingRule rule = RichTextMaskingUtil.createCustomRule( + "标签内敏感信息", + "敏感信息", + RichTextMaskingRule.MaskType.REPLACE, + "[已隐藏]"); - // 设置只在div标签中生效 - Set includeTags = new HashSet<>(); - includeTags.add("div"); - rule.setIncludeTags(includeTags); + // 设置只在div标签中生效 + final Set includeTags = new HashSet<>(); + includeTags.add("div"); + rule.setIncludeTags(includeTags); - processor.addRule(rule); + processor.addRule(rule); - // 测试HTML - String html = "

这是一段敏感信息

这也是一段敏感信息
"; - String masked = RichTextMaskingUtil.mask(html, processor); + // 测试HTML + final String html = "

这是一段敏感信息

这也是一段敏感信息
"; + final String masked = RichTextMaskingUtil.mask(html, processor); - // 验证只有div标签中的敏感信息被脱敏 - Assertions.assertTrue(masked.contains("

这是一段敏感信息

")); - Assertions.assertTrue(masked.contains("
这也是一段[已隐藏]
")); - } + // 验证只有div标签中的敏感信息被脱敏 + Assertions.assertTrue(masked.contains("

这是一段敏感信息

")); + Assertions.assertTrue(masked.contains("
这也是一段[已隐藏]
")); + } - @Test - public void testExcludeTags() { - // 创建自定义处理器 - RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); + @Test + public void testExcludeTags() { + // 创建自定义处理器 + final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); - // 创建排除特定标签的规则 - RichTextMaskingRule rule = RichTextMaskingUtil.createCustomRule( - "排除标签内敏感信息", - "敏感信息", - RichTextMaskingRule.MaskType.REPLACE, - "[已隐藏]"); + // 创建排除特定标签的规则 + final RichTextMaskingRule rule = RichTextMaskingUtil.createCustomRule( + "排除标签内敏感信息", + "敏感信息", + RichTextMaskingRule.MaskType.REPLACE, + "[已隐藏]"); - // 设置排除code标签 - rule.addExcludeTag("code"); + // 设置排除code标签 + rule.addExcludeTag("code"); - processor.addRule(rule); + processor.addRule(rule); - // 测试HTML - String html = "

这是一段敏感信息

这是代码中的敏感信息"; - String masked = RichTextMaskingUtil.mask(html, processor); + // 测试HTML + final String html = "

这是一段敏感信息

这是代码中的敏感信息"; + final String masked = RichTextMaskingUtil.mask(html, processor); - // 验证code标签中的敏感信息不被脱敏 - Assertions.assertTrue(masked.contains("

这是一段[已隐藏]

")); - Assertions.assertTrue(masked.contains("这是代码中的敏感信息")); - } + // 验证code标签中的敏感信息不被脱敏 + Assertions.assertTrue(masked.contains("

这是一段[已隐藏]

")); + Assertions.assertTrue(masked.contains("这是代码中的敏感信息")); + } - @Test - public void testComplexHtml() { - // 测试复杂HTML内容 - String html = "
" + - "

公司内部文档

" + - "

联系人:张三 zhangsan@example.com

" + - "

电话:13812345678

" + - "
这是一段机密信息,请勿外传
" + - "
// 这是一段代码\nString password = \"123456\";
" + - "

公司网址:https://www.example.com

" + - "
"; + @Test + public void testComplexHtml() { + // 测试复杂HTML内容 + final String html = "
" + + "

公司内部文档

" + + "

联系人:张三 zhangsan@example.com

" + + "

电话:13812345678

" + + "
这是一段机密信息,请勿外传
" + + "
// 这是一段代码\nString password = \"123456\";
" + + "

公司网址:https://www.example.com

" + + "
"; - // 创建自定义处理器 - RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); + // 创建自定义处理器 + final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); - // 添加邮箱脱敏规则 - processor.addRule(RichTextMaskingUtil.createEmailRule()); + // 添加邮箱脱敏规则 + processor.addRule(RichTextMaskingUtil.createEmailRule()); - // 添加手机号脱敏规则 - processor.addRule(RichTextMaskingUtil.createPartialMaskRule( - "手机号", - "1[3-9]\\d{9}", - 3, - 4, - '*')); + // 添加手机号脱敏规则 + processor.addRule(RichTextMaskingUtil.createPartialMaskRule( + "手机号", + "1[3-9]\\d{9}", + 3, + 4, + '*')); - // 添加敏感词脱敏规则 - processor.addRule(RichTextMaskingUtil.createSensitiveWordRule("机密|内部")); + // 添加敏感词脱敏规则 + processor.addRule(RichTextMaskingUtil.createSensitiveWordRule("机密|内部")); - // 添加网址脱敏规则 - processor.addRule(RichTextMaskingUtil.createUrlRule("[网址已隐藏]")); + // 添加网址脱敏规则 + processor.addRule(RichTextMaskingUtil.createUrlRule("[网址已隐藏]")); - // 添加密码脱敏规则,但排除code标签 - RichTextMaskingRule passwordRule = RichTextMaskingUtil.createCustomRule( - "密码", - "password = \"[^\"]+\"", - RichTextMaskingRule.MaskType.REPLACE, - "password = \"******\""); - passwordRule.addExcludeTag("code"); - processor.addRule(passwordRule); + // 添加密码脱敏规则,但排除code标签 + final RichTextMaskingRule passwordRule = RichTextMaskingUtil.createCustomRule( + "密码", + "password = \"[^\"]+\"", + RichTextMaskingRule.MaskType.REPLACE, + "password = \"******\""); + passwordRule.addExcludeTag("code"); + processor.addRule(passwordRule); - String masked = RichTextMaskingUtil.mask(html, processor); + final String masked = RichTextMaskingUtil.mask(html, processor); - // 验证结果 - Assertions.assertTrue(masked.contains("

公司**文档

")); - Assertions.assertTrue(masked.contains("z***")); - Assertions.assertTrue(masked.contains("138*****5678")); - Assertions.assertTrue(masked.contains("这是一段**信息")); - Assertions.assertTrue(masked.contains("String password = \"123456\"")); - Assertions.assertTrue(masked.contains("[网址已隐藏]")); - } + // 验证结果 + Assertions.assertTrue(masked.contains("

公司**文档

")); + Assertions.assertTrue(masked.contains("z***")); + Assertions.assertTrue(masked.contains("138****5678")); + Assertions.assertTrue(masked.contains("这是一段**信息")); + Assertions.assertFalse(masked.contains("String password = \"123456\"")); + Assertions.assertTrue(masked.contains("[网址已隐藏]")); + } }