package com.jnliok.sensitive_log.mask;
/**
* 高性能状态机解析器:O(n) 单次扫描,零正则,处理任意混合格式。
*
* <h3>支持的格式</h3>
* <ul>
* <li>纯文本:{@code "用户张三登录,手机13800138000"}</li>
* <li>双引号 JSON:{@code {"name":"张三","phone":"13800138000"}}</li>
* <li>单引号 JSON:{@code {'name':'张三','phone':'13800138000'}}</li>
* <li>无引号 JSON:{@code {name:张三, phone:13800138000}}</li>
* <li>Map.toString():{@code {name=张三, phone=13800138000}}</li>
* <li>混合格式:{@code log prefix {"name":"张三"} suffix}</li>
* </ul>
*
* <h3>设计</h3>
* 核心解析器 {@link #parseObject} 接受显式缓冲区参数,可安全递归。
* 顶层调用使用 ThreadLocal 缓冲区(零分配),嵌套对象递归时分配临时缓冲区。
* 消除了 mask/maskObjectInline 之间约 200 行的状态机重复代码。
*/
public final class HighPerfMaskParser {
// ---- 状态常量 ----
private static final int S_WAIT_KEY = 0; // 在 { 内,等待 key 或 }
private static final int S_IN_KEY_QUOTED = 1; // 正在读带引号 key
private static final int S_IN_KEY_UNQUOTED = 2; // 正在读无引号 key
private static final int S_AFTER_KEY = 3; // key 读完,等待 : 或 =
private static final int S_WAIT_VALUE = 4; // 等待 value 开始
private static final int S_IN_VALUE_QUOTED = 5; // 正在读带引号 value
private static final int S_IN_VALUE_UNQUOTED= 6; // 正在读无引号 value
// ---- ThreadLocal 缓冲区(顶层调用零分配) ----
private static final ThreadLocal<StringBuilder> SB =
ThreadLocal.withInitial(() -> new StringBuilder(4096));
private static final ThreadLocal<char[]> KEY_BUF =
ThreadLocal.withInitial(() -> new char[256]);
private static final ThreadLocal<char[]> VAL_BUF =
ThreadLocal.withInitial(() -> new char[4096]);
private HighPerfMaskParser() {}
// ==================== 公开接口 ====================
/**
* 对输入字符串进行脱敏处理
*/
public static String mask(String input, MaskingConfig config) {
if (input == null || input.isEmpty() || config.isEmpty()) return input;
StringBuilder out = SB.get();
out.setLength(0);
int len = input.length();
int i = 0;
while (i < len) {
char c = input.charAt(i);
if (c == '{') {
i = parseObject(input, i, out, KEY_BUF.get(), VAL_BUF.get(), config);
} else if (c == '<' && isXmlTag(input, i)) {
i = parseXmlElement(input, i, out, config);
} else {
out.append(c);
i++;
}
}
return out.toString();
}
// ==================== 核心:对象解析 ====================
/**
* 解析一个对象 { ... } 并对其中的敏感字段值进行脱敏。
* <p>
* 使用显式缓冲区参数,可安全递归——调用者栈上的缓冲区互不干扰。
*
* @param input 完整输入字符串
* @param start 指向 '{' 的位置
* @param out 输出缓冲区
* @param keyBuf 键名缓冲区
* @param valBuf 值缓冲区
* @param config 脱敏配置
* @return 匹配 '}' 之后的位置
*/
private static int parseObject(String input, int start, StringBuilder out,
char[] keyBuf, char[] valBuf,
MaskingConfig config) {
int len = input.length();
int i = start + 1; // 跳过 '{'
int depth = 1; // { } 嵌套深度
out.append('{');
int state = S_WAIT_KEY;
int keyLen = 0;
char keyQuote = 0;
MaskType maskType = null;
char valQuote = 0;
int valStart = -1;
int valLen = 0;
while (i < len && depth > 0) {
char c = input.charAt(i);
switch (state) {
// ============ S_WAIT_KEY ============
case S_WAIT_KEY:
if (c == '}') {
depth--;
out.append(c);
i++;
} else if (c == ',' || isWhitespace(c)) {
out.append(c);
i++;
} else if (c == '\'' || c == '"') {
keyQuote = c;
keyLen = 0;
out.append(c);
state = S_IN_KEY_QUOTED;
i++;
} else if (isKeyStartChar(c)) {
keyQuote = 0;
keyLen = 0;
out.append(c);
if (keyLen < keyBuf.length) keyBuf[keyLen++] = c;
state = S_IN_KEY_UNQUOTED;
i++;
} else {
out.append(c);
i++;
}
break;
// ============ S_IN_KEY_QUOTED ============
case S_IN_KEY_QUOTED:
if (c == '\\' && i + 1 < len) {
char next = input.charAt(i + 1);
out.append(c).append(next);
if (keyLen < keyBuf.length) keyBuf[keyLen++] = next;
i += 2;
} else if (c == keyQuote) {
out.append(c);
maskType = resolveKey(keyBuf, keyLen, config);
state = S_AFTER_KEY;
i++;
} else {
out.append(c);
if (keyLen < keyBuf.length) keyBuf[keyLen++] = c;
i++;
}
break;
// ============ S_IN_KEY_UNQUOTED ============
case S_IN_KEY_UNQUOTED:
if (c == ':' || c == '=' || c == '-') {
maskType = resolveKey(keyBuf, keyLen, config);
out.append(c);
state = S_WAIT_VALUE;
i++;
} else if (c == '{' || c == '[') {
// key 后紧跟 { 或 [,如 data-{...},key 结束,不消费 c
maskType = resolveKey(keyBuf, keyLen, config);
state = S_WAIT_VALUE;
break;
} else if (isWhitespace(c)) {
maskType = resolveKey(keyBuf, keyLen, config);
out.append(c);
state = S_AFTER_KEY;
i++;
} else {
out.append(c);
if (keyLen < keyBuf.length) keyBuf[keyLen++] = c;
i++;
}
break;
// ============ S_AFTER_KEY ============
case S_AFTER_KEY:
if (c == ':' || c == '=' || c == '-') {
out.append(c);
state = S_WAIT_VALUE;
} else if (isWhitespace(c)) {
out.append(c);
} else {
// 异常:key 后缺分隔符,回退
out.append(c);
maskType = null;
state = S_WAIT_KEY;
}
i++;
break;
// ============ S_WAIT_VALUE ============
case S_WAIT_VALUE:
if (isWhitespace(c)) {
out.append(c);
i++;
} else if (c == '\'' || c == '"') {
valQuote = c;
valStart = out.length();
out.append(c);
valLen = 0;
state = S_IN_VALUE_QUOTED;
i++;
} else if (c == '{') {
// 嵌套对象:分配局部缓冲区后递归
int end = parseObject(input, i, out, new char[256], new char[4096], config);
i = end;
maskType = null;
state = S_WAIT_KEY;
} else if (c == '[') {
// 嵌套数组:递归处理
i = parseArray(input, i, out, maskType, config);
maskType = null;
state = S_WAIT_KEY;
} else if (c == ',' || c == '}') {
// 空值,如 {key:,}
maskType = null;
state = S_WAIT_KEY;
break;
} else {
// 无引号 value
valStart = out.length();
valLen = 0;
if (valLen < valBuf.length) valBuf[valLen++] = c;
out.append(c);
state = S_IN_VALUE_UNQUOTED;
i++;
}
break;
// ============ S_IN_VALUE_QUOTED ============
case S_IN_VALUE_QUOTED:
if (c == '\\' && i + 1 < len) {
char next = input.charAt(i + 1);
out.append(c).append(next);
if (valLen < valBuf.length) valBuf[valLen++] = next;
i += 2;
} else if (c == valQuote) {
out.append(c);
applyMask(out, valStart + 1, out.length() - 1, valBuf, valLen, maskType);
maskType = null;
valLen = 0;
state = S_WAIT_KEY;
i++;
} else {
out.append(c);
if (valLen < valBuf.length) valBuf[valLen++] = c;
i++;
}
break;
// ============ S_IN_VALUE_UNQUOTED ============
case S_IN_VALUE_UNQUOTED:
if (c == ',' || c == '}') {
applyMask(out, valStart, out.length(), valBuf, valLen, maskType);
maskType = null;
valLen = 0;
state = S_WAIT_KEY;
break; // 不消费 c,让 S_WAIT_KEY 处理
} else if (c == ':' || isWhitespace(c)) {
applyMask(out, valStart, out.length(), valBuf, valLen, maskType);
maskType = null;
valLen = 0;
out.append(c);
state = S_WAIT_KEY;
i++;
} else {
out.append(c);
if (valLen < valBuf.length) valBuf[valLen++] = c;
i++;
}
break;
default:
i++;
break;
}
}
// 末尾未完成的 unquoted value
if (state == S_IN_VALUE_UNQUOTED && valLen > 0) {
applyMask(out, valStart, out.length(), valBuf, valLen, maskType);
}
return i;
}
// ==================== 核心:数组解析 ====================
/**
* 解析数组 [...] 并对其中的敏感字符串元素进行脱敏。
* 嵌套对象通过 {@link #parseObject} 递归处理。
*
* @param input 完整输入字符串
* @param start 指向 '[' 的位置
* @param out 输出缓冲区
* @param maskType 当前 key 对应的脱敏类型(null = 不脱敏)
* @param config 脱敏配置
* @return 匹配 ']' 之后的位置
*/
private static int parseArray(String input, int start, StringBuilder out,
MaskType maskType, MaskingConfig config) {
int len = input.length();
int depth = 1;
int i = start;
while (i < len && depth > 0) {
char c = input.charAt(i);
if (c == '[') {
depth++;
out.append(c);
i++;
} else if (c == ']') {
depth--;
out.append(c);
i++;
} else if (c == '{') {
i = parseObject(input, i, out, new char[256], new char[4096], config);
} else if (c == '\'' || c == '"') {
i = scanAndMaskQuotedArrayElement(input, i, out, maskType);
} else {
out.append(c);
i++;
}
}
return i;
}
/**
* 扫描数组中一个带引号的字符串元素,必要时脱敏。
*/
private static int scanAndMaskQuotedArrayElement(String input, int start,
StringBuilder out, MaskType maskType) {
int len = input.length();
int i = start;
char q = input.charAt(i);
out.append(q);
i++;
// 收集元素值(处理转义)
StringBuilder strVal = new StringBuilder();
while (i < len && input.charAt(i) != q) {
if (input.charAt(i) == '\\' && i + 1 < len) {
strVal.append(input.charAt(i + 1));
out.append(input.charAt(i)).append(input.charAt(i + 1));
i += 2;
} else {
strVal.append(input.charAt(i));
out.append(input.charAt(i));
i++;
}
}
if (i < len) {
out.append(input.charAt(i));
i++;
}
// 脱敏
if (maskType != null && strVal.length() > 0) {
String masked = MaskStrategyRegistry.getStrategy(maskType).mask(strVal.toString());
int valStartInOut = out.length() - strVal.length() - 1;
out.replace(valStartInOut, valStartInOut + strVal.length(), masked);
}
return i;
}
// ==================== 辅助方法 ====================
/**
* 解析 key 的脱敏类型。
*
* @return 脱敏类型,null 表示该 key 无需脱敏
*/
private static MaskType resolveKey(char[] keyBuf, int keyLen, MaskingConfig config) {
return config.tryMatch(keyBuf, 0, keyLen) != null
? config.getMaskType(new String(keyBuf, 0, keyLen))
: null;
}
/**
* 对 value 应用脱敏策略,原地替换 output 中的内容。
*/
private static void applyMask(StringBuilder out, int start, int end,
char[] valBuf, int valLen, MaskType maskType) {
if (maskType == null || valLen == 0) return;
String rawValue = new String(valBuf, 0, valLen);
String masked = MaskStrategyRegistry.getStrategy(maskType).mask(rawValue);
out.replace(start, end, masked);
}
// ==================== XML 解析 ====================
/**
* 判断 < 是否是一个 XML 元素的开始(非普通文本中的小于号)。
*/
private static boolean isXmlTag(String input, int pos) {
int next = pos + 1;
if (next >= input.length()) return false;
char c = input.charAt(next);
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| c == '_' || c == '/' || c == '?' || c == '!';
}
/**
* 解析一个 XML 元素并对其文本内容脱敏。
* <p>
* 支持:<tag>text</tag> / <tag/> / <?xml...?> / <!--...--> / <![CDATA[...]]>。
* 嵌套子元素递归处理。
*/
private static int parseXmlElement(String input, int start, StringBuilder out,
MaskingConfig config) {
int len = input.length();
int i = start;
if (i >= len) return i;
int next = i + 1;
if (next >= len) { out.append('<'); return next; }
char nc = input.charAt(next);
// ---- <?xml ...?> ----
if (nc == '?') {
return seekTo(input, i, out, "?>");
}
// ---- <!-- ... --> ----
if (nc == '!' && next + 2 < len
&& input.charAt(next + 1) == '-' && input.charAt(next + 2) == '-') {
return seekTo(input, i, out, "-->");
}
// ---- <![CDATA[...]]> ----
if (nc == '!' && next + 8 < len && input.charAt(next + 1) == '[') {
String cdata = input.substring(next + 2, next + 7);
if ("CDATA".equalsIgnoreCase(cdata) && input.charAt(next + 7) == '[') {
return seekTo(input, i, out, "]]>");
}
}
// ---- <!DOCTYPE ...> etc. ----
if (nc == '!') {
return seekTo(input, i, out, ">");
}
// ---- </closing> pass-through ----
if (nc == '/') {
return seekTo(input, i, out, ">");
}
// ============ <tagname ...>content</tagname> ============
i++; // skip '<'
// 读取标签名
int tagStart = i;
while (i < len) {
char tc = input.charAt(i);
if (tc == '>' || tc == '/' || tc <= ' ') break;
i++;
}
String tagName = input.substring(tagStart, i);
if (tagName.isEmpty()) { out.append('<'); return start + 1; }
// 自闭合 <tagname/>?
boolean selfClosing = false;
if (i < len && input.charAt(i) == '/') { selfClosing = true; i++; }
// 跳过属性直到 >
while (i < len && input.charAt(i) != '>') i++;
if (i >= len) { out.append('<').append(tagName); return i; }
i++; // skip '>'
// 输出开标签
out.append('<').append(tagName);
if (selfClosing) { out.append("/>"); return i; }
out.append('>');
// 查询脱敏类型
MaskType maskType = config.getMaskType(tagName);
// 读取内容直到 </tagname>
String closeTag = "</" + tagName + ">";
if (maskType == null) {
return scanXmlContent(input, i, out, closeTag, null, config);
} else {
return scanXmlContent(input, i, out, closeTag, maskType, config);
}
}
/**
* 扫描元素内容直到匹配 {@code closeTag},嵌套 XML 递归处理。
* 若 maskType 非 null 且内容为纯文本(无嵌套子元素),应用脱敏。
*
* <p>采用"延迟输出"策略:文本先收集到 textBuf,确定是否需要脱敏后再输出,
* 避免位置偏移导致的 IndexOutOfBoundsException。
*/
private static int scanXmlContent(String input, int start, StringBuilder out,
String closeTag, MaskType maskType,
MaskingConfig config) {
int len = input.length();
int i = start;
int clen = closeTag.length();
StringBuilder textBuf = new StringBuilder();
boolean hasNested = false;
while (i < len) {
if (input.charAt(i) == '<') {
// 检查是否匹配关闭标签
if (i + clen <= len) {
boolean match = true;
for (int j = 0; j < clen; j++) {
if (input.charAt(i + j) != closeTag.charAt(j)) { match = false; break; }
}
if (match) {
// 输出文本(必要时脱敏)
flushText(out, textBuf, maskType, hasNested);
out.append(closeTag);
return i + clen;
}
}
// 嵌套 XML 元素
if (isXmlTag(input, i)) {
flushText(out, textBuf, null, false); // 先输出已收集的文本
hasNested = true;
i = parseXmlElement(input, i, out, config);
continue;
}
}
textBuf.append(input.charAt(i));
i++;
}
// 到达末尾未找到关闭标签,输出剩余文本
flushText(out, textBuf, null, hasNested);
return i;
}
/**
* 将 textBuf 中的文本输出到 out,必要时脱敏。
*/
private static void flushText(StringBuilder out, StringBuilder textBuf,
MaskType maskType, boolean hasNested) {
if (textBuf.length() == 0) return;
if (maskType != null && !hasNested) {
out.append(MaskStrategyRegistry.getStrategy(maskType)
.mask(textBuf.toString()));
} else {
out.append(textBuf);
}
textBuf.setLength(0);
}
/**
* 从 start 输出直到 (并包含) delimiter,返回 delimiter 之后的位置。
*/
private static int seekTo(String input, int start, StringBuilder out, String delimiter) {
int dlen = delimiter.length();
int len = input.length();
// 找到 delimiter 的起始位置
int pos = start;
while (pos + dlen <= len) {
boolean match = true;
for (int j = 0; j < dlen; j++) {
if (input.charAt(pos + j) != delimiter.charAt(j)) { match = false; break; }
}
if (match) break;
pos++;
}
if (pos + dlen > len) {
// delimiter 未找到,输出剩余内容
out.append(input, start, len);
return len;
}
// 输出 start 到 delimiter 之后(包含 delimiter)
// Note: StringBuilder.append(s, start, end) 的 end 是绝对索引,不是长度
out.append(input, start, pos + dlen);
return pos + dlen;
}
// ---- 字符判断 ----
private static boolean isWhitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
private static boolean isKeyStartChar(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$';
}
}
package com.jnliok.sensitive_log.mask;
import java.util.*;
/**
* 脱敏配置管理器
* <p>
* 线程安全:构建后不可变,所有查询方法无副作用。
* 使用 HashMap 存储 fieldName → MaskType 映射,O(1) 查找。
*/
public final class MaskingConfig {
/** 空配置单例,避免重复创建 */
private static final MaskingConfig EMPTY = new MaskingConfig(Collections.emptyMap());
/** fieldName → MaskType 映射,构建后不可变 */
private final Map<String, MaskType> fieldMaskMap;
/** 使用字符数组存储已知字段名,供状态机快速前缀匹配 */
private final char[][] knownFieldChars;
/** 已知字段名的最大长度,用于状态机提前终止 */
private final int maxFieldNameLen;
/** 已知字段名的最小长度 */
private final int minFieldNameLen;
private MaskingConfig(Map<String, MaskType> fieldMaskMap) {
this.fieldMaskMap = Collections.unmodifiableMap(new HashMap<>(fieldMaskMap));
// 预计算字段名字符数组,用于快速匹配
this.knownFieldChars = new char[fieldMaskMap.size()][];
int i = 0;
int maxLen = 0;
int minLen = Integer.MAX_VALUE;
for (String name : fieldMaskMap.keySet()) {
knownFieldChars[i] = name.toCharArray();
i++;
if (name.length() > maxLen) maxLen = name.length();
if (name.length() < minLen) minLen = name.length();
}
this.maxFieldNameLen = maxLen;
this.minFieldNameLen = fieldMaskMap.isEmpty() ? 0 : minLen;
}
/**
* 创建构建器
*/
public static Builder builder() {
return new Builder();
}
/**
* 获取空配置
*/
public static MaskingConfig empty() {
return EMPTY;
}
/**
* 根据字段名查找脱敏类型,O(1)
*
* @param fieldName 字段名
* @return 对应的脱敏类型,未配置返回 null
*/
public MaskType getMaskType(String fieldName) {
return fieldMaskMap.get(fieldName);
}
/**
* 判断字段是否需要脱敏
*/
public boolean shouldMask(String fieldName) {
return fieldMaskMap.containsKey(fieldName);
}
/**
* 配置的字段数量
*/
public int size() {
return fieldMaskMap.size();
}
public boolean isEmpty() {
return fieldMaskMap.isEmpty();
}
// ---- 以下方法供状态机内部使用 ----
char[][] getKnownFieldChars() {
return knownFieldChars;
}
int getMaxFieldNameLen() {
return maxFieldNameLen;
}
int getMinFieldNameLen() {
return minFieldNameLen;
}
/**
* 尝试匹配字段名,O(k) 其中 k = 字段名长度
*
* @param buf 字符缓冲区
* @param offset 起始偏移
* @param length 字段名长度
* @return 匹配到的脱敏类型,未匹配返回 null
*/
MaskType tryMatch(char[] buf, int offset, int length) {
if (length < minFieldNameLen || length > maxFieldNameLen) {
return null;
}
// 遍历所有已知字段进行匹配(字段数量通常很少,线性遍历即可)
for (int i = 0; i < knownFieldChars.length; i++) {
char[] known = knownFieldChars[i];
if (known.length == length && charsEqual(known, buf, offset, length)) {
// 已知字段匹配成功,查 Map 获取 MaskType
return fieldMaskMap.get(new String(known));
}
}
return null;
}
private static boolean charsEqual(char[] a, char[] b, int bOffset, int len) {
for (int i = 0; i < len; i++) {
if (a[i] != b[bOffset + i]) {
return false;
}
}
return true;
}
// ==================== Builder ====================
public static class Builder {
private final Map<String, MaskType> fieldMaskMap = new HashMap<>();
/**
* 添加字段脱敏规则
*/
public Builder addField(String fieldName, MaskType maskType) {
fieldMaskMap.put(fieldName, maskType);
return this;
}
/**
* 批量添加字段规则
*/
public Builder addFields(Map<String, MaskType> fields) {
fieldMaskMap.putAll(fields);
return this;
}
public MaskingConfig build() {
return new MaskingConfig(new HashMap<>(fieldMaskMap));
}
}
}
package com.jnliok.sensitive_log.mask;
import org.apache.commons.lang3.StringUtils;
/**
* 脱敏策略接口
* <p>
* 所有实现必须是无状态的,以支持多线程并发调用。
* 输入输出均为字符串,内部使用字符数组操作,不使用正则。
*/
@FunctionalInterface
public interface MaskStrategy {
/**
* 对原始值进行脱敏处理
*
* @param rawValue 原始值(已去除引号和前后空白)
* @return 脱敏后的值
*/
String mask(String rawValue);
/**
* 全量脱敏:将字符串全部替换为 *
* <p>
* Java 17 String.repeat() 比手动 char[] 循环更简洁,JIT 优化后性能等同。
*/
default String fullMask(String s) {
return StringUtils.repeat('*',s.length());
}
}
package com.jnliok.sensitive_log.mask;
/**
* 脱敏策略注册表:将 MaskType 映射到具体的 MaskStrategy 实现
* <p>
* 线程安全:不可变,使用枚举单例策略实例
*/
public final class MaskStrategyRegistry {
private static final MaskStrategy[] STRATEGIES;
static {
MaskType[] types = MaskType.values();
STRATEGIES = new MaskStrategy[types.length];
STRATEGIES[MaskType.NAME.ordinal()] = NameMaskStrategy.INSTANCE;
STRATEGIES[MaskType.PHONE.ordinal()] = PhoneMaskStrategy.INSTANCE;
STRATEGIES[MaskType.ID_CARD.ordinal()] = IdCardMaskStrategy.INSTANCE;
STRATEGIES[MaskType.BANK_CARD.ordinal()] = BankCardMaskStrategy.INSTANCE;
STRATEGIES[MaskType.CVV.ordinal()] = CvvMaskStrategy.INSTANCE;
STRATEGIES[MaskType.TEL.ordinal()] = TelMaskStrategy.INSTANCE;
STRATEGIES[MaskType.EMAIL.ordinal()] = EmailMaskStrategy.INSTANCE;
STRATEGIES[MaskType.FULL_MASK.ordinal()] = CvvMaskStrategy.INSTANCE; // 全量*
STRATEGIES[MaskType.MIDDLE_MASK.ordinal()] = MiddleMaskStrategy.INSTANCE;
}
private MaskStrategyRegistry() {}
/**
* 根据 MaskType 获取策略,O(1)
*/
public static MaskStrategy getStrategy(MaskType type) {
return STRATEGIES[type.ordinal()];
}
// ---- 内部策略 ----
/**
* 中间脱敏:保留首尾各1位,中间用*替代
*/
private enum MiddleMaskStrategy implements MaskStrategy {
INSTANCE;
@Override
public String mask(String rawValue) {
if (rawValue == null || rawValue.isEmpty()) return rawValue;
int len = rawValue.length();
if (len <= 2) {
char[] r = new char[len];
for (int i = 0; i < len; i++) r[i] = '*';
return new String(r);
}
StringBuilder sb = new StringBuilder(len);
sb.append(rawValue.charAt(0));
for (int i = 1; i < len - 1; i++) sb.append('*');
sb.append(rawValue.charAt(len - 1));
return sb.toString();
}
}
}
package com.jnliok.sensitive_log.mask;
/**
* 手机号脱敏:保留前3后4,中间4位用****替代
* <p>
* 示例:13812345678 → 138****5678
* <p>
* 仅处理11位标准手机号,不足11位使用全量脱敏。
*/
public enum PhoneMaskStrategy implements MaskStrategy {
INSTANCE;
private static final int PHONE_LEN = 11;
@Override
public String mask(String rawValue) {
if (rawValue == null || rawValue.isEmpty()) return rawValue;
// 提取纯数字部分(可能包含空格、横线等)
char[] chars = rawValue.toCharArray();
int digitCount = 0;
for (char c : chars) {
if (c >= '0' && c <= '9') digitCount++;
}
if (digitCount < PHONE_LEN) {
return fullMask(rawValue);
}
// 保留前3后4,中间替换为*
char[] result = new char[chars.length];
int digitPos = 0;
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (c >= '0' && c <= '9') {
digitPos++;
if (digitPos <= 3 || digitPos > digitCount - 4) {
result[i] = c;
} else {
result[i] = '*';
}
} else {
result[i] = c;
}
}
return new String(result);
}
}
版权归属:
天明
许可协议:
本文使用《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》协议授权
评论区