文本及富文本支持自定义规则脱敏

This commit is contained in:
Looly 2025-06-09 17:21:35 +08:00
parent 04a0b342e4
commit c64e48e037
4 changed files with 810 additions and 705 deletions

View File

@ -10,153 +10,153 @@ import org.dromara.hutool.core.data.masking.RichTextMaskingRule;
*/ */
public class RichTextMaskingUtil { public class RichTextMaskingUtil {
/** /**
* 默认的富文本脱敏处理器 * 默认的富文本脱敏处理器
*/ */
private static final RichTextMaskingProcessor DEFAULT_PROCESSOR = createDefaultProcessor(); private static final RichTextMaskingProcessor DEFAULT_PROCESSOR = createDefaultProcessor();
/** /**
* 创建默认的富文本脱敏处理器 * 创建默认的富文本脱敏处理器
* *
* @return 默认的富文本脱敏处理器 * @return 默认的富文本脱敏处理器
*/ */
private static RichTextMaskingProcessor createDefaultProcessor() { private static RichTextMaskingProcessor createDefaultProcessor() {
RichTextMaskingProcessor processor = new RichTextMaskingProcessor(true); final RichTextMaskingProcessor processor = new RichTextMaskingProcessor(true);
// 添加一些常用的脱敏规则 // 添加一些常用的脱敏规则
// 邮箱脱敏规则 // 邮箱脱敏规则
processor.addRule(new RichTextMaskingRule( processor.addRule(new RichTextMaskingRule(
"邮箱", "邮箱",
"[\\w.-]+@[\\w.-]+\\.\\w+", "[\\w.-]+@[\\w.-]+\\.\\w+",
RichTextMaskingRule.MaskType.PARTIAL, RichTextMaskingRule.MaskType.PARTIAL,
"[邮箱已隐藏]") "[邮箱已隐藏]")
.setPreserveLeft(1) .setPreserveLeft(1)
.setPreserveRight(0) .setPreserveRight(0)
.setMaskChar('*')); .setMaskChar('*'));
// 网址脱敏规则 // 网址脱敏规则
processor.addRule(new RichTextMaskingRule( processor.addRule(new RichTextMaskingRule(
"网址", "网址",
"https?://[\\w.-]+(?:/[\\w.-]*)*", "https?://[\\w.-]+(?:/[\\w.-]*)*",
RichTextMaskingRule.MaskType.REPLACE, RichTextMaskingRule.MaskType.REPLACE,
"[网址已隐藏]")); "[网址已隐藏]"));
// 敏感词脱敏规则示例 // 敏感词脱敏规则示例
processor.addRule(new RichTextMaskingRule( processor.addRule(new RichTextMaskingRule(
"敏感词", "敏感词",
"(机密|绝密|内部资料|秘密|保密)", "(机密|绝密|内部资料|秘密|保密)",
RichTextMaskingRule.MaskType.FULL, RichTextMaskingRule.MaskType.FULL,
"***") "***")
.setMaskChar('*')); .setMaskChar('*'));
return processor; return processor;
} }
/** /**
* 对富文本内容进行脱敏处理 * 对富文本内容进行脱敏处理
* *
* @param text 富文本内容 * @param text 富文本内容
* @return 脱敏后的文本 * @return 脱敏后的文本
*/ */
public static String mask(String text) { public static String mask(final String text) {
return DEFAULT_PROCESSOR.mask(text); return DEFAULT_PROCESSOR.mask(text);
} }
/** /**
* 使用自定义处理器对富文本内容进行脱敏处理 * 使用自定义处理器对富文本内容进行脱敏处理
* *
* @param text 富文本内容 * @param text 富文本内容
* @param processor 自定义处理器 * @param processor 自定义处理器
* @return 脱敏后的文本 * @return 脱敏后的文本
*/ */
public static String mask(String text, RichTextMaskingProcessor processor) { public static String mask(final String text, final RichTextMaskingProcessor processor) {
return processor.mask(text); return processor.mask(text);
} }
/** /**
* 创建一个新的富文本脱敏处理器 * 创建一个新的富文本脱敏处理器
* *
* @param preserveHtmlTags 是否保留HTML标签 * @param preserveHtmlTags 是否保留HTML标签
* @return 富文本脱敏处理器 * @return 富文本脱敏处理器
*/ */
public static RichTextMaskingProcessor createProcessor(boolean preserveHtmlTags) { public static RichTextMaskingProcessor createProcessor(final boolean preserveHtmlTags) {
return new RichTextMaskingProcessor(preserveHtmlTags); return new RichTextMaskingProcessor(preserveHtmlTags);
} }
/** /**
* 创建一个邮箱脱敏规则 * 创建一个邮箱脱敏规则
* *
* @return 邮箱脱敏规则 * @return 邮箱脱敏规则
*/ */
public static RichTextMaskingRule createEmailRule() { public static RichTextMaskingRule createEmailRule() {
return new RichTextMaskingRule( return new RichTextMaskingRule(
"邮箱", "邮箱",
"[\\w.-]+@[\\w.-]+\\.\\w+", "[\\w.-]+@[\\w.-]+\\.\\w+",
RichTextMaskingRule.MaskType.PARTIAL, RichTextMaskingRule.MaskType.PARTIAL,
null) null)
.setPreserveLeft(1) .setPreserveLeft(1)
.setPreserveRight(0) .setPreserveRight(0)
.setMaskChar('*'); .setMaskChar('*');
} }
/** /**
* 创建一个网址脱敏规则 * 创建一个网址脱敏规则
* *
* @param replacement 替换文本 * @param replacement 替换文本
* @return 网址脱敏规则 * @return 网址脱敏规则
*/ */
public static RichTextMaskingRule createUrlRule(String replacement) { public static RichTextMaskingRule createUrlRule(final String replacement) {
return new RichTextMaskingRule( return new RichTextMaskingRule(
"网址", "网址",
"https?://[\\w.-]+(?:/[\\w.-]*)*", "https?://[\\w.-]+(?:/[\\w.-]*)*",
RichTextMaskingRule.MaskType.REPLACE, RichTextMaskingRule.MaskType.REPLACE,
replacement); replacement);
} }
/** /**
* 创建一个敏感词脱敏规则 * 创建一个敏感词脱敏规则
* *
* @param pattern 敏感词正则表达式 * @param pattern 敏感词正则表达式
* @return 敏感词脱敏规则 * @return 敏感词脱敏规则
*/ */
public static RichTextMaskingRule createSensitiveWordRule(String pattern) { public static RichTextMaskingRule createSensitiveWordRule(final String pattern) {
return new RichTextMaskingRule( return new RichTextMaskingRule(
"敏感词", "敏感词",
pattern, pattern,
RichTextMaskingRule.MaskType.FULL, RichTextMaskingRule.MaskType.FULL,
null) null)
.setMaskChar('*'); .setMaskChar('*');
} }
/** /**
* 创建一个自定义脱敏规则 * 创建一个自定义脱敏规则
* *
* @param name 规则名称 * @param name 规则名称
* @param pattern 匹配模式正则表达式 * @param pattern 匹配模式正则表达式
* @param maskType 脱敏类型 * @param maskType 脱敏类型
* @param replacement 替换内容 * @param replacement 替换内容
* @return 自定义脱敏规则 * @return 自定义脱敏规则
*/ */
public static RichTextMaskingRule createCustomRule(String name, String pattern, public static RichTextMaskingRule createCustomRule(final String name, final String pattern,
RichTextMaskingRule.MaskType maskType, final RichTextMaskingRule.MaskType maskType,
String replacement) { final String replacement) {
return new RichTextMaskingRule(name, pattern, maskType, replacement); return new RichTextMaskingRule(name, pattern, maskType, replacement);
} }
/** /**
* 创建一个部分脱敏规则 * 创建一个部分脱敏规则
* *
* @param name 规则名称 * @param name 规则名称
* @param pattern 匹配模式正则表达式 * @param pattern 匹配模式正则表达式
* @param preserveLeft 保留左侧字符数 * @param preserveLeft 保留左侧字符数
* @param preserveRight 保留右侧字符数 * @param preserveRight 保留右侧字符数
* @param maskChar 脱敏字符 * @param maskChar 脱敏字符
* @return 部分脱敏规则 * @return 部分脱敏规则
*/ */
public static RichTextMaskingRule createPartialMaskRule(String name, String pattern, public static RichTextMaskingRule createPartialMaskRule(final String name, final String pattern,
int preserveLeft, int preserveRight, final int preserveLeft, final int preserveRight,
char maskChar) { final char maskChar) {
return new RichTextMaskingRule(name, pattern, preserveLeft, preserveRight, maskChar); return new RichTextMaskingRule(name, pattern, preserveLeft, preserveRight, maskChar);
} }
} }

View File

@ -14,299 +14,283 @@ import java.util.regex.Pattern;
*/ */
public class RichTextMaskingProcessor { public class RichTextMaskingProcessor {
/** /**
* 脱敏规则列表 * 脱敏规则列表
*/ */
private final List<RichTextMaskingRule> rules = new ArrayList<>(); private final List<RichTextMaskingRule> rules = new ArrayList<>();
/** /**
* 是否保留HTML标签 * 是否保留HTML标签
*/ */
private boolean preserveHtmlTags = true; 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标签 * @param rule 脱敏规则
*/ * @return this
public RichTextMaskingProcessor(boolean preserveHtmlTags) { */
this.preserveHtmlTags = preserveHtmlTags; public RichTextMaskingProcessor addRule(final RichTextMaskingRule rule) {
} this.rules.add(rule);
return this;
}
/** /**
* 添加脱敏规则 * 对文本内容进行脱敏处理
* *
* @param rule 脱敏规则 * @param text 文本内容
* @return this * @return 脱敏后的文本
*/ */
public RichTextMaskingProcessor addRule(RichTextMaskingRule rule) { public String mask(final String text) {
this.rules.add(rule); if (StrUtil.isBlank(text)) {
return this; return text;
} }
/** // 如果是HTML内容则需要特殊处理
* 设置默认的脱敏字符 if (preserveHtmlTags && isHtmlContent(text)) {
* return maskHtmlContent(text);
* @param defaultMaskChar 默认的脱敏字符 } else {
* @return this // 普通文本直接处理
*/ return maskPlainText(text);
public RichTextMaskingProcessor setDefaultMaskChar(char defaultMaskChar) { }
this.defaultMaskChar = defaultMaskChar; }
return this;
}
/** /**
* 对文本内容进行脱敏处理 * 判断是否为HTML内容
* *
* @param text 文本内容 * @param text 文本内容
* @return 脱敏后的文本 * @return 是否为HTML内容
*/ */
public String mask(String text) { private boolean isHtmlContent(final String text) {
if (StrUtil.isBlank(text)) { // 简单判断是否包含HTML标签
return text; return text.contains("<") && text.contains(">") &&
} (text.contains("</") || text.contains("/>"));
}
// 如果是HTML内容则需要特殊处理 /**
if (preserveHtmlTags && isHtmlContent(text)) { * 对HTML内容进行脱敏处理
return maskHtmlContent(text); *
} else { * @param html HTML内容
// 普通文本直接处理 * @return 脱敏后的HTML
return maskPlainText(text); */
} private String maskHtmlContent(final String html) {
} final StringBuilder result = new StringBuilder();
int lastIndex = 0;
boolean inTag = false;
String currentTag = null;
/** for (int i = 0; i < html.length(); i++) {
* 判断是否为HTML内容 final char c = html.charAt(i);
*
* @param text 文本内容
* @return 是否为HTML内容
*/
private boolean isHtmlContent(String text) {
// 简单判断是否包含HTML标签
return text.contains("<") && text.contains(">") &&
(text.contains("</") || text.contains("/>"));
}
/** if (c == '<') {
* 对HTML内容进行脱敏处理 // 处理标签前的文本内容
* if (!inTag && i > lastIndex) {
* @param html HTML内容 final String textContent = html.substring(lastIndex, i);
* @return 脱敏后的HTML result.append(processTextContentWithContext(textContent, currentTag));
*/ }
private String maskHtmlContent(String html) {
StringBuilder result = new StringBuilder();
int lastIndex = 0;
boolean inTag = false;
String currentTag = null;
for (int i = 0; i < html.length(); i++) { inTag = true;
char c = html.charAt(i); lastIndex = i;
if (c == '<') { // 尝试获取当前标签名
// 处理标签前的文本内容 int tagNameStart = i + 1;
if (!inTag && i > lastIndex) { if (tagNameStart < html.length()) {
String textContent = html.substring(lastIndex, i); // 跳过结束标签的斜杠
result.append(processTextContentWithContext(textContent, currentTag)); if (html.charAt(tagNameStart) == '/') {
} tagNameStart++;
}
inTag = true; // 查找标签名结束位置
lastIndex = i; int tagNameEnd = html.indexOf(' ', tagNameStart);
if (tagNameEnd == -1) {
tagNameEnd = html.indexOf('>', tagNameStart);
}
// 尝试获取当前标签名 if (tagNameEnd > tagNameStart) {
int tagNameStart = i + 1; currentTag = html.substring(tagNameStart, tagNameEnd).toLowerCase();
if (tagNameStart < html.length()) { }
// 跳过结束标签的斜杠 }
if (html.charAt(tagNameStart) == '/') { } else if (c == '>' && inTag) {
tagNameStart++; inTag = false;
} result.append(html, lastIndex, i + 1); // 保留标签
lastIndex = i + 1;
}
}
// 查找标签名结束位置 // 处理最后一部分
int tagNameEnd = html.indexOf(' ', tagNameStart); if (lastIndex < html.length()) {
if (tagNameEnd == -1) { if (inTag) {
tagNameEnd = html.indexOf('>', tagNameStart); // 如果还在标签内直接添加剩余部分
} result.append(html.substring(lastIndex));
} else {
// 处理最后的文本内容
final String textContent = html.substring(lastIndex);
result.append(processTextContentWithContext(textContent, currentTag));
}
}
if (tagNameEnd > tagNameStart) { return result.toString();
currentTag = html.substring(tagNameStart, tagNameEnd).toLowerCase(); }
}
}
} else if (c == '>' && inTag) {
inTag = false;
result.append(html, lastIndex, i + 1); // 保留标签
lastIndex = i + 1;
}
}
// 处理最后一部分 /**
if (lastIndex < html.length()) { * 根据上下文处理文本内容
if (inTag) { *
// 如果还在标签内直接添加剩余部分 * @param text 文本内容
result.append(html.substring(lastIndex)); * @param tagName 当前所在的标签名
} else { * @return 处理后的文本
// 处理最后的文本内容 */
String textContent = html.substring(lastIndex); private String processTextContentWithContext(final String text, final String tagName) {
result.append(processTextContentWithContext(textContent, currentTag)); if (StrUtil.isBlank(text)) {
} return text;
} }
return result.toString(); String result = text;
}
/** for (final RichTextMaskingRule rule : rules) {
* 根据上下文处理文本内容 // 检查是否需要根据标签进行过滤
* if (tagName != null) {
* @param text 文本内容 // 如果设置了只包含特定标签且当前标签不在列表中则跳过
* @param tagName 当前所在的标签名 if (!rule.getIncludeTags().isEmpty() && !rule.getIncludeTags().contains(tagName)) {
* @return 处理后的文本 continue;
*/ }
private String processTextContentWithContext(String text, String tagName) {
if (StrUtil.isBlank(text)) {
return text;
}
String result = text; // 如果当前标签在排除列表中则跳过
if (rule.getExcludeTags().contains(tagName)) {
continue;
}
}
for (RichTextMaskingRule rule : rules) { // 应用脱敏规则
// 检查是否需要根据标签进行过滤 result = applyMaskingRule(result, rule);
if (tagName != null) { }
// 如果设置了只包含特定标签且当前标签不在列表中则跳过
if (!rule.getIncludeTags().isEmpty() && !rule.getIncludeTags().contains(tagName)) {
continue;
}
// 如果当前标签在排除列表中则跳过 return result;
if (rule.getExcludeTags().contains(tagName)) { }
continue;
}
}
// 应用脱敏规则 /**
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);
}
/** return result;
* 对普通文本进行脱敏处理 }
*
* @param text 文本内容
* @return 脱敏后的文本
*/
private String maskPlainText(String text) {
String result = text;
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);
/** final StringBuffer sb = new StringBuffer();
* 应用脱敏规则
*
* @param text 文本内容
* @param rule 脱敏规则
* @return 脱敏后的文本
*/
private String applyMaskingRule(String text, RichTextMaskingRule rule) {
if (StrUtil.isBlank(text) || StrUtil.isBlank(rule.getPattern())) {
return text;
}
Pattern pattern = Pattern.compile(rule.getPattern()); while (matcher.find()) {
Matcher matcher = pattern.matcher(text); 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()) { case PARTIAL:
String matched = matcher.group(); // 部分脱敏保留部分原始内容
String replacement; replacement = partialMask(matched, rule.getPreserveLeft(), rule.getPreserveRight(), rule.getMaskChar());
break;
switch (rule.getMaskType()) { case REPLACE:
case FULL: // 替换脱敏用指定文本替换
// 完全脱敏用脱敏字符替换整个匹配内容 replacement = rule.getReplacement();
replacement = StrUtil.repeat(rule.getMaskChar(), matched.length()); break;
break;
case PARTIAL: default:
// 部分脱敏保留部分原始内容 replacement = matched;
replacement = partialMask(matched, rule.getPreserveLeft(), rule.getPreserveRight(), rule.getMaskChar()); break;
break; }
case REPLACE: // 处理正则表达式中的特殊字符
// 替换脱敏用指定文本替换 matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
replacement = rule.getReplacement(); }
break;
default: matcher.appendTail(sb);
replacement = matched;
break;
}
// 处理正则表达式中的特殊字符 return sb.toString();
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); }
}
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();
}
/** // 调整保留字符数确保不超过文本长度
* 部分脱敏保留部分原始内容 preserveLeft = Math.min(preserveLeft, length);
* preserveRight = Math.min(preserveRight, length - preserveLeft);
* @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;
}
int length = text.length(); // 计算需要脱敏的字符数
final int maskLength = length - preserveLeft - preserveRight;
// 调整保留字符数确保不超过文本长度 if (maskLength <= 0) {
preserveLeft = Math.min(preserveLeft, length); return text;
preserveRight = Math.min(preserveRight, length - preserveLeft); }
// 计算需要脱敏的字符数 final StringBuilder sb = new StringBuilder(length);
int maskLength = length - preserveLeft - preserveRight;
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) { if (preserveRight > 0) {
sb.append(text, 0, preserveLeft); sb.append(text, length - preserveRight, length);
} }
// 添加脱敏字符 return sb.toString();
sb.append(StrUtil.repeat(maskChar, maskLength)); }
// 添加右侧保留的字符
if (preserveRight > 0) {
sb.append(text, length - preserveRight, length);
}
return sb.toString();
}
} }

View File

@ -10,214 +10,335 @@ import java.util.Set;
*/ */
public class RichTextMaskingRule { public class RichTextMaskingRule {
/** /**
* 脱敏类型枚举 * 脱敏类型枚举
*/ */
public enum MaskType { public enum MaskType {
/** /**
* 完全脱敏将匹配的内容完全替换为指定字符 * 完全脱敏将匹配的内容完全替换为指定字符
*/ */
FULL, 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类型 * 保留左侧字符数用于PARTIAL类型
*/ */
private int preserveLeft; private int preserveLeft;
/** /**
* 保留右侧字符数用于PARTIAL类型 * 保留右侧字符数用于PARTIAL类型
*/ */
private int preserveRight; private int preserveRight;
/** /**
* 脱敏字符 * 脱敏字符
*/ */
private char maskChar = '*'; private char maskChar = '*';
/** /**
* 是否处理HTML标签内容 * 是否处理HTML标签内容
*/ */
private boolean processHtmlTags = false; private boolean processHtmlTags = false;
/** /**
* 需要排除的HTML标签 * 需要排除的HTML标签
*/ */
private Set<String> excludeTags = new HashSet<>(); private Set<String> excludeTags = new HashSet<>();
/** /**
* 仅处理指定的HTML标签 * 仅处理指定的HTML标签
*/ */
private Set<String> includeTags = new HashSet<>(); private Set<String> includeTags = new HashSet<>();
/** /**
* 构造函数 * 构造函数
*/ */
public RichTextMaskingRule() { public RichTextMaskingRule() {
} }
/** /**
* 构造函数 * 构造函数
* *
* @param name 规则名称 * @param name 规则名称
* @param pattern 匹配模式正则表达式 * @param pattern 匹配模式正则表达式
* @param maskType 脱敏类型 * @param maskType 脱敏类型
* @param replacement 替换内容 * @param replacement 替换内容
*/ */
public RichTextMaskingRule(String name, String pattern, MaskType maskType, String replacement) { public RichTextMaskingRule(final String name, final String pattern, final MaskType maskType, final String replacement) {
this.name = name; this.name = name;
this.pattern = pattern; this.pattern = pattern;
this.maskType = maskType; this.maskType = maskType;
this.replacement = replacement; this.replacement = replacement;
} }
/** /**
* 构造函数用于部分脱敏 * 构造函数用于部分脱敏
* *
* @param name 规则名称 * @param name 规则名称
* @param pattern 匹配模式正则表达式 * @param pattern 匹配模式正则表达式
* @param preserveLeft 保留左侧字符数 * @param preserveLeft 保留左侧字符数
* @param preserveRight 保留右侧字符数 * @param preserveRight 保留右侧字符数
* @param maskChar 脱敏字符 * @param maskChar 脱敏字符
*/ */
public RichTextMaskingRule(String name, String pattern, int preserveLeft, int preserveRight, char maskChar) { public RichTextMaskingRule(final String name, final String pattern, final int preserveLeft, final int preserveRight, final char maskChar) {
this.name = name; this.name = name;
this.pattern = pattern; this.pattern = pattern;
this.maskType = MaskType.PARTIAL; this.maskType = MaskType.PARTIAL;
this.preserveLeft = preserveLeft; this.preserveLeft = preserveLeft;
this.preserveRight = preserveRight; this.preserveRight = preserveRight;
this.maskChar = maskChar; 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; * 获取是否处理HTML标签内容
return this; *
} * @return 是否处理HTML标签内容
*/
public boolean isProcessHtmlTags() {
return processHtmlTags;
}
public Set<String> getExcludeTags() { /**
return excludeTags; * 设置是否处理HTML标签内容
} *
* @param processHtmlTags 是否处理HTML标签内容
* @return this
*/
public RichTextMaskingRule setProcessHtmlTags(final boolean processHtmlTags) {
this.processHtmlTags = processHtmlTags;
return this;
}
public RichTextMaskingRule setExcludeTags(Set<String> excludeTags) { /**
this.excludeTags = excludeTags; * 获取需要排除的HTML标签
return this; *
} * @return 需要排除的HTML标签
*/
public Set<String> getExcludeTags() {
return excludeTags;
}
public RichTextMaskingRule addExcludeTag(String tag) { /**
this.excludeTags.add(tag.toLowerCase()); * 设置需要排除的HTML标签
return this; *
} * @param excludeTags 需要排除的HTML标签
* @return this
*/
public RichTextMaskingRule setExcludeTags(final Set<String> excludeTags) {
this.excludeTags = excludeTags;
return this;
}
public Set<String> 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<String> includeTags) { /**
this.includeTags = includeTags; * 获取仅处理指定的HTML标签
return this; *
} * @return 仅处理指定的HTML标签
*/
public Set<String> getIncludeTags() {
return includeTags;
}
public RichTextMaskingRule addIncludeTag(String tag) { /**
this.includeTags.add(tag.toLowerCase()); * 设置仅处理指定的HTML标签
return this; *
} * @param includeTags 仅处理指定的HTML标签
* @return this
*/
public RichTextMaskingRule setIncludeTags(final Set<String> 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;
}
} }

View File

@ -15,188 +15,188 @@ import java.util.Set;
*/ */
public class RichTextMaskingUtilTest { public class RichTextMaskingUtilTest {
@Test @Test
public void testDefaultMask() { public void testDefaultMask() {
// 测试默认脱敏功能 // 测试默认脱敏功能
String html = "这是一封邮件联系人test@example.com网址https://www.example.com包含机密信息。"; final String html = "这是一封邮件联系人test@example.com网址https://www.example.com包含机密信息。";
String masked = RichTextMaskingUtil.mask(html); final String masked = RichTextMaskingUtil.mask(html);
// 验证邮箱被脱敏 // 验证邮箱被脱敏
Assertions.assertFalse(masked.contains("test@example.com")); Assertions.assertFalse(masked.contains("test@example.com"));
Assertions.assertTrue(masked.contains("t***")); Assertions.assertTrue(masked.contains("t***"));
// 验证网址被脱敏 // 验证网址被脱敏
Assertions.assertFalse(masked.contains("https://www.example.com")); Assertions.assertFalse(masked.contains("https://www.example.com"));
Assertions.assertTrue(masked.contains("[网址已隐藏]")); Assertions.assertTrue(masked.contains("[网址已隐藏]"));
// 验证敏感词被脱敏 // 验证敏感词被脱敏
Assertions.assertFalse(masked.contains("机密")); Assertions.assertFalse(masked.contains("机密"));
Assertions.assertTrue(masked.contains("**")); Assertions.assertTrue(masked.contains("**"));
} }
@Test @Test
public void testHtmlContentMask() { public void testHtmlContentMask() {
// 测试HTML内容脱敏 // 测试HTML内容脱敏
String html = "<p>这是一封邮件,联系人:<a href='mailto:test@example.com'>test@example.com</a>" + final String html = "<p>这是一封邮件,联系人:<a href='mailto:testA@example.com'>test@example.com</a>" +
"网址:<a href='https://www.example.com'>https://www.example.com</a>" + "网址:<a href='https://www.aexample.com'>https://www.example.com</a>" +
"包含<span style='color:red'>机密</span>信息。</p>"; "包含<span style='color:red'>机密</span>信息。</p>";
String masked = RichTextMaskingUtil.mask(html); final String masked = RichTextMaskingUtil.mask(html);
// 验证HTML标签被保留 // 验证HTML标签被保留
Assertions.assertTrue(masked.contains("<p>")); Assertions.assertTrue(masked.contains("<p>"));
Assertions.assertTrue(masked.contains("</p>")); Assertions.assertTrue(masked.contains("</p>"));
Assertions.assertTrue(masked.contains("<a href='mailto:")); Assertions.assertTrue(masked.contains("<a href='mailto:"));
Assertions.assertTrue(masked.contains("<span style='color:red'>")); Assertions.assertTrue(masked.contains("<span style='color:red'>"));
// 验证邮箱被脱敏 // 验证邮箱被脱敏
Assertions.assertFalse(masked.contains("test@example.com")); Assertions.assertFalse(masked.contains("test@example.com"));
Assertions.assertTrue(masked.contains("t***")); Assertions.assertTrue(masked.contains("t***"));
// 验证网址被脱敏 // 验证网址被脱敏
Assertions.assertFalse(masked.contains("https://www.example.com")); Assertions.assertFalse(masked.contains("https://www.example.com"));
Assertions.assertTrue(masked.contains("[网址已隐藏]")); Assertions.assertTrue(masked.contains("[网址已隐藏]"));
// 验证敏感词被脱敏 // 验证敏感词被脱敏
Assertions.assertFalse(masked.contains("机密")); Assertions.assertFalse(masked.contains("机密"));
Assertions.assertTrue(masked.contains("**")); Assertions.assertTrue(masked.contains("**"));
} }
@Test @Test
public void testCustomProcessor() { public void testCustomProcessor() {
// 创建自定义处理器 // 创建自定义处理器
RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true);
// 添加自定义规则 - 手机号码 // 添加自定义规则 - 手机号码
processor.addRule(RichTextMaskingUtil.createPartialMaskRule( processor.addRule(RichTextMaskingUtil.createPartialMaskRule(
"手机号", "手机号",
"1[3-9]\\d{9}", "1[3-9]\\d{9}",
3, 3,
4, 4,
'*')); '*'));
// 添加自定义规则 - 公司名称 // 添加自定义规则 - 公司名称
processor.addRule(RichTextMaskingUtil.createCustomRule( processor.addRule(RichTextMaskingUtil.createCustomRule(
"公司名称", "公司名称",
"XX科技有限公司", "XX科技有限公司",
RichTextMaskingRule.MaskType.REPLACE, RichTextMaskingRule.MaskType.REPLACE,
"[公司名称已隐藏]")); "[公司名称已隐藏]"));
// 测试文本 // 测试文本
String text = "联系电话13812345678公司名称XX科技有限公司"; final String text = "联系电话13812345678公司名称XX科技有限公司";
String masked = RichTextMaskingUtil.mask(text, processor); final String masked = RichTextMaskingUtil.mask(text, processor);
// 验证手机号被脱敏 // 验证手机号被脱敏
Assertions.assertFalse(masked.contains("13812345678")); Assertions.assertFalse(masked.contains("13812345678"));
Assertions.assertTrue(masked.contains("138*****5678")); Assertions.assertTrue(masked.contains("138****5678"));
// 验证公司名称被脱敏 // 验证公司名称被脱敏
Assertions.assertFalse(masked.contains("XX科技有限公司")); Assertions.assertFalse(masked.contains("XX科技有限公司"));
Assertions.assertTrue(masked.contains("[公司名称已隐藏]")); Assertions.assertTrue(masked.contains("[公司名称已隐藏]"));
} }
@Test @Test
public void testTagFiltering() { public void testTagFiltering() {
// 创建自定义处理器 // 创建自定义处理器
RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true);
// 创建只在特定标签中生效的规则 // 创建只在特定标签中生效的规则
RichTextMaskingRule rule = RichTextMaskingUtil.createCustomRule( final RichTextMaskingRule rule = RichTextMaskingUtil.createCustomRule(
"标签内敏感信息", "标签内敏感信息",
"敏感信息", "敏感信息",
RichTextMaskingRule.MaskType.REPLACE, RichTextMaskingRule.MaskType.REPLACE,
"[已隐藏]"); "[已隐藏]");
// 设置只在div标签中生效 // 设置只在div标签中生效
Set<String> includeTags = new HashSet<>(); final Set<String> includeTags = new HashSet<>();
includeTags.add("div"); includeTags.add("div");
rule.setIncludeTags(includeTags); rule.setIncludeTags(includeTags);
processor.addRule(rule); processor.addRule(rule);
// 测试HTML // 测试HTML
String html = "<p>这是一段敏感信息</p><div>这也是一段敏感信息</div>"; final String html = "<p>这是一段敏感信息</p><div>这也是一段敏感信息</div>";
String masked = RichTextMaskingUtil.mask(html, processor); final String masked = RichTextMaskingUtil.mask(html, processor);
// 验证只有div标签中的敏感信息被脱敏 // 验证只有div标签中的敏感信息被脱敏
Assertions.assertTrue(masked.contains("<p>这是一段敏感信息</p>")); Assertions.assertTrue(masked.contains("<p>这是一段敏感信息</p>"));
Assertions.assertTrue(masked.contains("<div>这也是一段[已隐藏]</div>")); Assertions.assertTrue(masked.contains("<div>这也是一段[已隐藏]</div>"));
} }
@Test @Test
public void testExcludeTags() { public void testExcludeTags() {
// 创建自定义处理器 // 创建自定义处理器
RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true);
// 创建排除特定标签的规则 // 创建排除特定标签的规则
RichTextMaskingRule rule = RichTextMaskingUtil.createCustomRule( final RichTextMaskingRule rule = RichTextMaskingUtil.createCustomRule(
"排除标签内敏感信息", "排除标签内敏感信息",
"敏感信息", "敏感信息",
RichTextMaskingRule.MaskType.REPLACE, RichTextMaskingRule.MaskType.REPLACE,
"[已隐藏]"); "[已隐藏]");
// 设置排除code标签 // 设置排除code标签
rule.addExcludeTag("code"); rule.addExcludeTag("code");
processor.addRule(rule); processor.addRule(rule);
// 测试HTML // 测试HTML
String html = "<p>这是一段敏感信息</p><code>这是代码中的敏感信息</code>"; final String html = "<p>这是一段敏感信息</p><code>这是代码中的敏感信息</code>";
String masked = RichTextMaskingUtil.mask(html, processor); final String masked = RichTextMaskingUtil.mask(html, processor);
// 验证code标签中的敏感信息不被脱敏 // 验证code标签中的敏感信息不被脱敏
Assertions.assertTrue(masked.contains("<p>这是一段[已隐藏]</p>")); Assertions.assertTrue(masked.contains("<p>这是一段[已隐藏]</p>"));
Assertions.assertTrue(masked.contains("<code>这是代码中的敏感信息</code>")); Assertions.assertTrue(masked.contains("<code>这是代码中的敏感信息</code>"));
} }
@Test @Test
public void testComplexHtml() { public void testComplexHtml() {
// 测试复杂HTML内容 // 测试复杂HTML内容
String html = "<div class='content'>" + final String html = "<div class='content'>" +
"<h1>公司内部文档</h1>" + "<h1>公司内部文档</h1>" +
"<p>联系人:张三 <a href='mailto:zhangsan@example.com'>zhangsan@example.com</a></p>" + "<p>联系人:张三 <a href='mailto:zhangsan@example.com'>zhangsan@example.com</a></p>" +
"<p>电话13812345678</p>" + "<p>电话13812345678</p>" +
"<div class='secret'>这是一段机密信息,请勿外传</div>" + "<div class='secret'>这是一段机密信息,请勿外传</div>" +
"<pre><code>// 这是一段代码\nString password = \"123456\";</code></pre>" + "<pre><code>// 这是一段代码\nString password = \"123456\";</code></pre>" +
"<p>公司网址:<a href='https://www.example.com'>https://www.example.com</a></p>" + "<p>公司网址:<a href='https://www.example.com'>https://www.example.com</a></p>" +
"</div>"; "</div>";
// 创建自定义处理器 // 创建自定义处理器
RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true); final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true);
// 添加邮箱脱敏规则 // 添加邮箱脱敏规则
processor.addRule(RichTextMaskingUtil.createEmailRule()); processor.addRule(RichTextMaskingUtil.createEmailRule());
// 添加手机号脱敏规则 // 添加手机号脱敏规则
processor.addRule(RichTextMaskingUtil.createPartialMaskRule( processor.addRule(RichTextMaskingUtil.createPartialMaskRule(
"手机号", "手机号",
"1[3-9]\\d{9}", "1[3-9]\\d{9}",
3, 3,
4, 4,
'*')); '*'));
// 添加敏感词脱敏规则 // 添加敏感词脱敏规则
processor.addRule(RichTextMaskingUtil.createSensitiveWordRule("机密|内部")); processor.addRule(RichTextMaskingUtil.createSensitiveWordRule("机密|内部"));
// 添加网址脱敏规则 // 添加网址脱敏规则
processor.addRule(RichTextMaskingUtil.createUrlRule("[网址已隐藏]")); processor.addRule(RichTextMaskingUtil.createUrlRule("[网址已隐藏]"));
// 添加密码脱敏规则但排除code标签 // 添加密码脱敏规则但排除code标签
RichTextMaskingRule passwordRule = RichTextMaskingUtil.createCustomRule( final RichTextMaskingRule passwordRule = RichTextMaskingUtil.createCustomRule(
"密码", "密码",
"password = \"[^\"]+\"", "password = \"[^\"]+\"",
RichTextMaskingRule.MaskType.REPLACE, RichTextMaskingRule.MaskType.REPLACE,
"password = \"******\""); "password = \"******\"");
passwordRule.addExcludeTag("code"); passwordRule.addExcludeTag("code");
processor.addRule(passwordRule); processor.addRule(passwordRule);
String masked = RichTextMaskingUtil.mask(html, processor); final String masked = RichTextMaskingUtil.mask(html, processor);
// 验证结果 // 验证结果
Assertions.assertTrue(masked.contains("<h1>公司**文档</h1>")); Assertions.assertTrue(masked.contains("<h1>公司**文档</h1>"));
Assertions.assertTrue(masked.contains("z***")); Assertions.assertTrue(masked.contains("z***"));
Assertions.assertTrue(masked.contains("138*****5678")); Assertions.assertTrue(masked.contains("138****5678"));
Assertions.assertTrue(masked.contains("这是一段**信息")); Assertions.assertTrue(masked.contains("这是一段**信息"));
Assertions.assertTrue(masked.contains("String password = \"123456\"")); Assertions.assertFalse(masked.contains("String password = \"123456\""));
Assertions.assertTrue(masked.contains("[网址已隐藏]")); Assertions.assertTrue(masked.contains("[网址已隐藏]"));
} }
} }