diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java
index 41a934751..df4a71ad7 100644
--- a/hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java
@@ -16,8 +16,9 @@
package org.dromara.hutool.core.data;
+import org.dromara.hutool.core.data.masking.MaskingManager;
+import org.dromara.hutool.core.data.masking.MaskingType;
import org.dromara.hutool.core.text.StrUtil;
-import org.dromara.hutool.core.text.CharUtil;
/**
* 数据脱敏(Data Masking)工具类,对某些敏感信息(比如,身份证号、手机号、卡号、姓名、地址、邮箱等 )屏蔽敏感数据。
@@ -44,74 +45,6 @@ import org.dromara.hutool.core.text.CharUtil;
*/
public class MaskingUtil {
- /**
- * 支持的脱敏类型枚举
- *
- * @author dazer and neusoft and qiaomu
- */
- public enum MaskingType {
- /**
- * 用户id
- */
- USER_ID,
- /**
- * 中文名
- */
- CHINESE_NAME,
- /**
- * 身份证号
- */
- ID_CARD,
- /**
- * 座机号
- */
- FIXED_PHONE,
- /**
- * 手机号
- */
- MOBILE_PHONE,
- /**
- * 地址
- */
- ADDRESS,
- /**
- * 电子邮件
- */
- EMAIL,
- /**
- * 密码
- */
- PASSWORD,
- /**
- * 中国大陆车牌,包含普通车辆、新能源车辆
- */
- CAR_LICENSE,
- /**
- * 银行卡
- */
- BANK_CARD,
- /**
- * IPv4地址
- */
- IPV4,
- /**
- * IPv6地址
- */
- IPV6,
- /**
- * 定义了一个first_mask的规则,只显示第一个字符。
- */
- FIRST_MASK,
- /**
- * 清空为null
- */
- CLEAR_TO_NULL,
- /**
- * 清空为""
- */
- CLEAR_TO_EMPTY
- }
-
/**
* 脱敏,使用默认的脱敏策略
*
@@ -134,60 +67,8 @@ public class MaskingUtil {
* @author dazer and neusoft and qiaomu
* @since 5.6.2
*/
- public static String masking(final CharSequence str, final MaskingType maskingType) {
- if (StrUtil.isBlank(str)) {
- return StrUtil.EMPTY;
- }
- String newStr = String.valueOf(str);
- switch (maskingType) {
- case USER_ID:
- newStr = String.valueOf(userId());
- break;
- case CHINESE_NAME:
- newStr = chineseName(String.valueOf(str));
- break;
- case ID_CARD:
- newStr = idCardNum(String.valueOf(str), 1, 2);
- break;
- case FIXED_PHONE:
- newStr = fixedPhone(String.valueOf(str));
- break;
- case MOBILE_PHONE:
- newStr = mobilePhone(String.valueOf(str));
- break;
- case ADDRESS:
- newStr = address(String.valueOf(str), 8);
- break;
- case EMAIL:
- newStr = email(String.valueOf(str));
- break;
- case PASSWORD:
- newStr = password(String.valueOf(str));
- break;
- case CAR_LICENSE:
- newStr = carLicense(String.valueOf(str));
- break;
- case BANK_CARD:
- newStr = bankCard(String.valueOf(str));
- break;
- case IPV4:
- newStr = ipv4(String.valueOf(str));
- break;
- case IPV6:
- newStr = ipv6(String.valueOf(str));
- break;
- case FIRST_MASK:
- newStr = firstMask(String.valueOf(str));
- break;
- case CLEAR_TO_EMPTY:
- newStr = clear();
- break;
- case CLEAR_TO_NULL:
- newStr = clearToNull();
- break;
- default:
- }
- return newStr;
+ public static String masking(final MaskingType maskingType, final CharSequence str) {
+ return MaskingManager.getInstance().masking(maskingType.name(), str);
}
/**
@@ -219,6 +100,16 @@ public class MaskingUtil {
return 0L;
}
+ /**
+ * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**
+ *
+ * @param fullName 姓名
+ * @return 脱敏后的姓名
+ */
+ public static String chineseName(final CharSequence fullName) {
+ return firstMask(fullName);
+ }
+
/**
* 定义了一个first_mask的规则,只显示第一个字符。
* 脱敏前:123456789;脱敏后:1********。
@@ -226,21 +117,8 @@ public class MaskingUtil {
* @param str 字符串
* @return 脱敏后的字符串
*/
- public static String firstMask(final String str) {
- if (StrUtil.isBlank(str)) {
- return StrUtil.EMPTY;
- }
- return StrUtil.hide(str, 1, str.length());
- }
-
- /**
- * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**
- *
- * @param fullName 姓名
- * @return 脱敏后的姓名
- */
- public static String chineseName(final String fullName) {
- return firstMask(fullName);
+ public static String firstMask(final CharSequence str) {
+ return MaskingManager.EMPTY.firstMask(str);
}
/**
@@ -251,20 +129,8 @@ public class MaskingUtil {
* @param end 保留:后面的end位数;从1开始
* @return 脱敏后的身份证
*/
- public static String idCardNum(final String idCardNum, final int front, final int end) {
- //身份证不能为空
- if (StrUtil.isBlank(idCardNum)) {
- return StrUtil.EMPTY;
- }
- //需要截取的长度不能大于身份证号长度
- if ((front + end) > idCardNum.length()) {
- return StrUtil.EMPTY;
- }
- //需要截取的不能小于0
- if (front < 0 || end < 0) {
- return StrUtil.EMPTY;
- }
- return StrUtil.hide(idCardNum, front, idCardNum.length() - end);
+ public static String idCardNum(final CharSequence idCardNum, final int front, final int end) {
+ return MaskingManager.EMPTY.idCardNum(idCardNum, front, end);
}
/**
@@ -273,11 +139,8 @@ public class MaskingUtil {
* @param num 固定电话
* @return 脱敏后的固定电话;
*/
- public static String fixedPhone(final String num) {
- if (StrUtil.isBlank(num)) {
- return StrUtil.EMPTY;
- }
- return StrUtil.hide(num, 4, num.length() - 2);
+ public static String fixedPhone(final CharSequence num) {
+ return MaskingManager.EMPTY.fixedPhone(num);
}
/**
@@ -286,11 +149,8 @@ public class MaskingUtil {
* @param num 移动电话;
* @return 脱敏后的移动电话;
*/
- public static String mobilePhone(final String num) {
- if (StrUtil.isBlank(num)) {
- return StrUtil.EMPTY;
- }
- return StrUtil.hide(num, 3, num.length() - 4);
+ public static String mobilePhone(final CharSequence num) {
+ return MaskingManager.EMPTY.mobilePhone(num);
}
/**
@@ -300,12 +160,8 @@ public class MaskingUtil {
* @param sensitiveSize 敏感信息长度
* @return 脱敏后的家庭地址
*/
- public static String address(final String address, final int sensitiveSize) {
- if (StrUtil.isBlank(address)) {
- return StrUtil.EMPTY;
- }
- final int length = address.length();
- return StrUtil.hide(address, length - sensitiveSize, length);
+ public static String address(final CharSequence address, final int sensitiveSize) {
+ return MaskingManager.EMPTY.address(address, sensitiveSize);
}
/**
@@ -314,28 +170,23 @@ public class MaskingUtil {
* @param email 邮箱
* @return 脱敏后的邮箱
*/
- public static String email(final String email) {
- if (StrUtil.isBlank(email)) {
- return StrUtil.EMPTY;
- }
- final int index = StrUtil.indexOf(email, '@');
- if (index <= 1) {
- return email;
- }
- return StrUtil.hide(email, 1, index);
+ public static String email(final CharSequence email) {
+ return MaskingManager.EMPTY.email(email);
}
/**
- * 【密码】密码的全部字符都用*代替,比如:******
+ * 【密码】密码的全部字符都用*代替,比如:******
+ * 密码位数不能被猜测,因此固定10位
*
* @param password 密码
* @return 脱敏后的密码
*/
- public static String password(final String password) {
+ public static String password(final CharSequence password) {
if (StrUtil.isBlank(password)) {
return StrUtil.EMPTY;
}
- return StrUtil.repeat('*', password.length());
+ // 密码位数不能被猜测,因此固定10位
+ return StrUtil.repeat('*', 10);
}
/**
@@ -349,18 +200,8 @@ public class MaskingUtil {
* @param carLicense 完整的车牌号
* @return 脱敏后的车牌
*/
- public static String carLicense(String carLicense) {
- if (StrUtil.isBlank(carLicense)) {
- return StrUtil.EMPTY;
- }
- // 普通车牌
- if (carLicense.length() == 7) {
- carLicense = StrUtil.hide(carLicense, 3, 6);
- } else if (carLicense.length() == 8) {
- // 新能源车牌
- carLicense = StrUtil.hide(carLicense, 3, 7);
- }
- return carLicense;
+ public static String carLicense(final CharSequence carLicense) {
+ return MaskingManager.EMPTY.carLicense(carLicense);
}
/**
@@ -371,29 +212,8 @@ public class MaskingUtil {
* @return 脱敏之后的银行卡号
* @since 5.6.3
*/
- public static String bankCard(String bankCardNo) {
- if (StrUtil.isBlank(bankCardNo)) {
- return bankCardNo;
- }
- bankCardNo = StrUtil.cleanBlank(bankCardNo);
- if (bankCardNo.length() < 9) {
- return bankCardNo;
- }
-
- final int length = bankCardNo.length();
- final int endLength = length % 4 == 0 ? 4 : length % 4;
- final int midLength = length - 4 - endLength;
- final StringBuilder buf = new StringBuilder();
-
- buf.append(bankCardNo, 0, 4);
- for (int i = 0; i < midLength; ++i) {
- if (i % 4 == 0) {
- buf.append(CharUtil.SPACE);
- }
- buf.append('*');
- }
- buf.append(CharUtil.SPACE).append(bankCardNo, length - endLength, length);
- return buf.toString();
+ public static String bankCard(final CharSequence bankCardNo) {
+ return MaskingManager.EMPTY.bankCard(bankCardNo);
}
/**
@@ -402,8 +222,8 @@ public class MaskingUtil {
* @param ipv4 IPv4地址
* @return 脱敏后的地址
*/
- public static String ipv4(final String ipv4) {
- return StrUtil.subBefore(ipv4, '.', false) + ".*.*.*";
+ public static String ipv4(final CharSequence ipv4) {
+ return MaskingManager.EMPTY.ipv4(ipv4);
}
/**
@@ -412,7 +232,7 @@ public class MaskingUtil {
* @param ipv6 IPv6地址
* @return 脱敏后的地址
*/
- public static String ipv6(final String ipv6) {
- return StrUtil.subBefore(ipv6, ':', false) + ":*:*:*:*:*:*:*";
+ public static String ipv6(final CharSequence ipv6) {
+ return MaskingManager.EMPTY.ipv6(ipv6);
}
}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingHandler.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingHandler.java
new file mode 100644
index 000000000..d7d2ff733
--- /dev/null
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingHandler.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2024 Hutool Team and hutool.cn
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.hutool.core.data.masking;
+
+/**
+ * 脱敏处理器,用于自定义脱敏规则
+ *
+ */
+@FunctionalInterface
+public interface MaskingHandler {
+
+ /**
+ * 处理传入的数据字符串,经过脱敏逻辑后,返回处理后的值
+ *
+ * @param value 待处理的值
+ * @return 处理后的值
+ */
+ String handle(CharSequence value);
+}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingManager.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingManager.java
new file mode 100644
index 000000000..67aef9d85
--- /dev/null
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingManager.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (c) 2024 Hutool Team and hutool.cn
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.hutool.core.data.masking;
+
+import org.dromara.hutool.core.text.CharUtil;
+import org.dromara.hutool.core.text.StrUtil;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static org.dromara.hutool.core.data.masking.MaskingType.*;
+
+/**
+ * 脱敏管理器,用于管理所有脱敏处理器,使用方式有三种:
+ *
+ * - 全局默认:使用{@link MaskingManager#getInstance()},带有预定义的脱敏方法
+ * - 自定义默认:使用{@link MaskingManager#ofDefault(char)},可以自定义脱敏字符,带有预定义的脱敏方法
+ * - 自定义:使用{@link #MaskingManager(Map, char)}构造,不带有默认规则
+ *
+ *
+ * @author Looly
+ */
+public class MaskingManager {
+
+ /**
+ * 默认的脱敏字符:*
+ */
+ public static final char DEFAULT_MASK_CHAR = '*';
+
+ /**
+ * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载
+ */
+ private static class SingletonHolder {
+ /**
+ * 静态初始化器,由JVM来保证线程安全
+ */
+ private static final MaskingManager INSTANCE = registerDefault(DEFAULT_MASK_CHAR);
+ }
+
+ /**
+ * 空脱敏管理器,用于不处理任何数据的情况
+ */
+ public static final MaskingManager EMPTY = new MaskingManager(null);
+
+ /**
+ * 获得单例的 MaskingManager
+ *
+ * @return MaskingManager
+ */
+ public static MaskingManager getInstance() {
+ return SingletonHolder.INSTANCE;
+ }
+
+ /**
+ * 创建默认的脱敏管理器,通过给定的脱敏字符,提供默认的脱敏规则
+ *
+ * @param maskChar 脱敏字符,默认为*
+ * @return 默认的脱敏管理器
+ */
+ public static MaskingManager ofDefault(final char maskChar) {
+ return registerDefault(maskChar);
+ }
+
+ private final Map handlerMap;
+ private final char maskChar;
+
+ /**
+ * 构造
+ *
+ * @param handlerMap 脱敏处理器Map,如果应用于单例,则需要传入线程安全的Map
+ */
+ public MaskingManager(final Map handlerMap) {
+ this(handlerMap, DEFAULT_MASK_CHAR);
+ }
+
+ /**
+ * 构造
+ *
+ * @param handlerMap 脱敏处理器Map,如果应用于单例,则需要传入线程安全的Map
+ * @param maskChar 默认的脱敏字符,默认为*
+ */
+ public MaskingManager(final Map handlerMap, final char maskChar) {
+ this.handlerMap = handlerMap;
+ this.maskChar = maskChar;
+ }
+
+ /**
+ * 注册一个脱敏处理器
+ *
+ * @param type 类型
+ * @param handler 脱敏处理器
+ * @return this
+ */
+ public MaskingManager register(final String type, final MaskingHandler handler) {
+ this.handlerMap.put(type, handler);
+ return this;
+ }
+
+ /**
+ * 脱敏处理
+ * 如果没有指定的脱敏处理器,则返回{@code null}
+ *
+ * @param type 类型
+ * @param value 待脱敏值
+ * @return 脱敏后的值
+ */
+ public String masking(String type, final CharSequence value) {
+ if (StrUtil.isEmpty(type)) {
+ type = CLEAR_TO_NULL.name();
+ }
+ final MaskingHandler handler = handlerMap.get(type);
+ return null == handler ? null : handler.handle(value);
+ }
+
+ /**
+ * 默认的脱敏处理器注册
+ *
+ * @param maskChar 默认的脱敏字符,默认为*
+ * @return 默认的脱敏处理器注册
+ */
+ private static MaskingManager registerDefault(final char maskChar) {
+ final MaskingManager manager = new MaskingManager(
+ new ConcurrentHashMap<>(15, 1), maskChar);
+
+ manager.register(USER_ID.name(), (str) -> "0");
+ manager.register(CHINESE_NAME.name(), manager::firstMask);
+ manager.register(ID_CARD.name(), (str) -> manager.idCardNum(str, 1, 2));
+ manager.register(FIXED_PHONE.name(), manager::fixedPhone);
+ manager.register(MOBILE_PHONE.name(), manager::mobilePhone);
+ manager.register(ADDRESS.name(), (str) -> manager.address(str, 8));
+ manager.register(EMAIL.name(), manager::email);
+ manager.register(PASSWORD.name(), manager::password);
+ manager.register(CAR_LICENSE.name(), manager::carLicense);
+ manager.register(BANK_CARD.name(), manager::bankCard);
+ manager.register(IPV4.name(), manager::ipv4);
+ manager.register(IPV6.name(), manager::ipv6);
+ manager.register(FIRST_MASK.name(), manager::firstMask);
+ manager.register(CLEAR_TO_EMPTY.name(), (str) -> StrUtil.EMPTY);
+ manager.register(CLEAR_TO_NULL.name(), (str) -> null);
+
+ return manager;
+ }
+
+ /**
+ * 定义了一个first_mask的规则,只显示第一个字符。
+ * 脱敏前:123456789;脱敏后:1********。
+ *
+ * @param str 字符串
+ * @return 脱敏后的字符串
+ */
+ public String firstMask(final CharSequence str) {
+ if (StrUtil.isBlank(str)) {
+ return StrUtil.EMPTY;
+ }
+ return StrUtil.replaceByCodePoint(str, 1, str.length(), maskChar);
+ }
+
+ /**
+ * 【身份证号】前1位 和后2位
+ *
+ * @param idCardNum 身份证
+ * @param front 保留:前面的front位数;从1开始
+ * @param end 保留:后面的end位数;从1开始
+ * @return 脱敏后的身份证
+ */
+ public String idCardNum(final CharSequence idCardNum, final int front, final int end) {
+ //身份证不能为空
+ if (StrUtil.isBlank(idCardNum)) {
+ return StrUtil.EMPTY;
+ }
+ //需要截取的长度不能大于身份证号长度
+ if ((front + end) > idCardNum.length()) {
+ return StrUtil.EMPTY;
+ }
+ //需要截取的不能小于0
+ if (front < 0 || end < 0) {
+ return StrUtil.EMPTY;
+ }
+ return StrUtil.replaceByCodePoint(idCardNum, front, idCardNum.length() - end, maskChar);
+ }
+
+ /**
+ * 【固定电话 前四位,后两位
+ *
+ * @param num 固定电话
+ * @return 脱敏后的固定电话;
+ */
+ public String fixedPhone(final CharSequence num) {
+ if (StrUtil.isBlank(num)) {
+ return StrUtil.EMPTY;
+ }
+ return StrUtil.replaceByCodePoint(num, 4, num.length() - 2, maskChar);
+ }
+
+ /**
+ * 【手机号码】前三位,后4位,其他隐藏,比如135****2210
+ *
+ * @param num 移动电话;
+ * @return 脱敏后的移动电话;
+ */
+ public String mobilePhone(final CharSequence num) {
+ if (StrUtil.isBlank(num)) {
+ return StrUtil.EMPTY;
+ }
+ return StrUtil.replaceByCodePoint(num, 3, num.length() - 4, maskChar);
+ }
+
+ /**
+ * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****
+ *
+ * @param address 家庭住址
+ * @param sensitiveSize 敏感信息长度
+ * @return 脱敏后的家庭地址
+ */
+ public String address(final CharSequence address, final int sensitiveSize) {
+ if (StrUtil.isBlank(address)) {
+ return StrUtil.EMPTY;
+ }
+ final int length = address.length();
+ return StrUtil.replaceByCodePoint(address, length - sensitiveSize, length, maskChar);
+ }
+
+ /**
+ * 【电子邮箱】邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com
+ *
+ * @param email 邮箱
+ * @return 脱敏后的邮箱
+ */
+ public String email(final CharSequence email) {
+ if (StrUtil.isBlank(email)) {
+ return StrUtil.EMPTY;
+ }
+ final int index = StrUtil.indexOf(email, '@');
+ if (index <= 1) {
+ return email.toString();
+ }
+ return StrUtil.replaceByCodePoint(email, 1, index, maskChar);
+ }
+
+ /**
+ * 【密码】密码的全部字符都用定义的脱敏字符(如*)代替,比如:******
+ * 密码位数不能被猜测,因此固定10位
+ *
+ * @param password 密码
+ * @return 脱敏后的密码
+ */
+ public String password(final CharSequence password) {
+ if (StrUtil.isBlank(password)) {
+ return StrUtil.EMPTY;
+ }
+ // 密码位数不能被猜测,因此固定10位
+ return StrUtil.repeat(maskChar, 10);
+ }
+
+ /**
+ * 【中国车牌】车牌中间用脱敏字符(如*)代替
+ * eg1:null -》 ""
+ * eg1:"" -》 ""
+ * eg3:苏D40000 -》 苏D4***0
+ * eg4:陕A12345D -》 陕A1****D
+ * eg5:京A123 -》 京A123 如果是错误的车牌,不处理
+ *
+ * @param carLicense 完整的车牌号
+ * @return 脱敏后的车牌
+ */
+ public String carLicense(CharSequence carLicense) {
+ if (StrUtil.isBlank(carLicense)) {
+ return StrUtil.EMPTY;
+ }
+ // 普通车牌
+ if (carLicense.length() == 7) {
+ carLicense = StrUtil.replaceByCodePoint(carLicense, 3, 6, maskChar);
+ } else if (carLicense.length() == 8) {
+ // 新能源车牌
+ carLicense = StrUtil.replaceByCodePoint(carLicense, 3, 7, maskChar);
+ }
+ return carLicense.toString();
+ }
+
+ /**
+ * 银行卡号脱敏
+ * eg: 1101 **** **** **** 3256
+ *
+ * @param bankCardNo 银行卡号
+ * @return 脱敏之后的银行卡号
+ */
+ public String bankCard(CharSequence bankCardNo) {
+ if (StrUtil.isBlank(bankCardNo)) {
+ return StrUtil.toStringOrNull(bankCardNo);
+ }
+ bankCardNo = StrUtil.cleanBlank(bankCardNo);
+ if (bankCardNo.length() < 9) {
+ return bankCardNo.toString();
+ }
+
+ final int length = bankCardNo.length();
+ final int endLength = length % 4 == 0 ? 4 : length % 4;
+ final int midLength = length - 4 - endLength;
+ final StringBuilder buf = new StringBuilder();
+
+ buf.append(bankCardNo, 0, 4);
+ for (int i = 0; i < midLength; ++i) {
+ if (i % 4 == 0) {
+ buf.append(CharUtil.SPACE);
+ }
+ buf.append(maskChar);
+ }
+ buf.append(CharUtil.SPACE).append(bankCardNo, length - endLength, length);
+ return buf.toString();
+ }
+
+ /**
+ * IPv4脱敏,如:脱敏前:192.0.2.1;脱敏后:192.*.*.*。
+ *
+ * @param ipv4 IPv4地址
+ * @return 脱敏后的地址
+ */
+ public String ipv4(final CharSequence ipv4) {
+ return StrUtil.subBefore(ipv4, '.', false) + StrUtil.repeat("." + maskChar, 3);
+ }
+
+ /**
+ * IPv6脱敏,如:脱敏前:2001:0db8:86a3:08d3:1319:8a2e:0370:7344;脱敏后:2001:*:*:*:*:*:*:*
+ *
+ * @param ipv6 IPv6地址
+ * @return 脱敏后的地址
+ */
+ public String ipv6(final CharSequence ipv6) {
+ return StrUtil.subBefore(ipv6, ':', false) + StrUtil.repeat(":" + maskChar, 7);
+ }
+}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingType.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingType.java
new file mode 100644
index 000000000..21e83f74e
--- /dev/null
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/MaskingType.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2024 Hutool Team and hutool.cn
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.hutool.core.data.masking;
+
+/**
+ * 支持的脱敏类型枚举
+ *
+ * @author dazer and neusoft and qiaomu
+ */
+public enum MaskingType {
+ /**
+ * 用户id
+ */
+ USER_ID,
+ /**
+ * 中文名
+ */
+ CHINESE_NAME,
+ /**
+ * 身份证号
+ */
+ ID_CARD,
+ /**
+ * 座机号
+ */
+ FIXED_PHONE,
+ /**
+ * 手机号
+ */
+ MOBILE_PHONE,
+ /**
+ * 地址
+ */
+ ADDRESS,
+ /**
+ * 电子邮件
+ */
+ EMAIL,
+ /**
+ * 密码
+ */
+ PASSWORD,
+ /**
+ * 中国大陆车牌,包含普通车辆、新能源车辆
+ */
+ CAR_LICENSE,
+ /**
+ * 银行卡
+ */
+ BANK_CARD,
+ /**
+ * IPv4地址
+ */
+ IPV4,
+ /**
+ * IPv6地址
+ */
+ IPV6,
+ /**
+ * 定义了一个first_mask的规则,只显示第一个字符。
+ */
+ FIRST_MASK,
+ /**
+ * 清空为null
+ */
+ CLEAR_TO_NULL,
+ /**
+ * 清空为""
+ */
+ CLEAR_TO_EMPTY
+}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/package-info.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/package-info.java
new file mode 100644
index 000000000..50fe35904
--- /dev/null
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/masking/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2024 Hutool Team and hutool.cn
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * 数据脱敏,提供各种数据类型(字符串、数字等)的脱敏方法。
+ *
+ * @author looly
+ */
+package org.dromara.hutool.core.data.masking;
diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java
index 2df776899..efbc8e3f2 100644
--- a/hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java
+++ b/hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java
@@ -16,9 +16,13 @@
package org.dromara.hutool.core.data;
+import org.dromara.hutool.core.data.masking.MaskingManager;
+import org.dromara.hutool.core.data.masking.MaskingType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
/**
* 脱敏工具类 MaskingUtil 安全测试
*
@@ -29,94 +33,108 @@ public class MaskingUtilTest {
@Test
public void maskingTest() {
- Assertions.assertEquals("", MaskingUtil.masking("100", MaskingUtil.MaskingType.CLEAR_TO_EMPTY));
- Assertions.assertNull(MaskingUtil.masking("100", MaskingUtil.MaskingType.CLEAR_TO_NULL));
+ assertEquals("", MaskingUtil.masking(MaskingType.CLEAR_TO_EMPTY, "100"));
+ Assertions.assertNull(MaskingUtil.masking(MaskingType.CLEAR_TO_NULL, "100"));
- Assertions.assertEquals("0", MaskingUtil.masking("100", MaskingUtil.MaskingType.USER_ID));
- Assertions.assertEquals("段**", MaskingUtil.masking("段正淳", MaskingUtil.MaskingType.CHINESE_NAME));
- Assertions.assertEquals("5***************1X", MaskingUtil.masking("51343620000320711X", MaskingUtil.MaskingType.ID_CARD));
- Assertions.assertEquals("0915*****79", MaskingUtil.masking("09157518479", MaskingUtil.MaskingType.FIXED_PHONE));
- Assertions.assertEquals("180****1999", MaskingUtil.masking("18049531999", MaskingUtil.MaskingType.MOBILE_PHONE));
- Assertions.assertEquals("北京市海淀区马********", MaskingUtil.masking("北京市海淀区马连洼街道289号", MaskingUtil.MaskingType.ADDRESS));
- Assertions.assertEquals("d*************@gmail.com.cn", MaskingUtil.masking("duandazhi-jack@gmail.com.cn", MaskingUtil.MaskingType.EMAIL));
- Assertions.assertEquals("**********", MaskingUtil.masking("1234567890", MaskingUtil.MaskingType.PASSWORD));
+ assertEquals("0", MaskingUtil.masking(MaskingType.USER_ID, "100"));
+ assertEquals("段**", MaskingUtil.masking(MaskingType.CHINESE_NAME, "段正淳"));
+ assertEquals("5***************1X", MaskingUtil.masking(MaskingType.ID_CARD, "51343620000320711X"));
+ assertEquals("0915*****79", MaskingUtil.masking(MaskingType.FIXED_PHONE, "09157518479"));
+ assertEquals("180****1999", MaskingUtil.masking(MaskingType.MOBILE_PHONE, "18049531999"));
+ assertEquals("北京市海淀区马********", MaskingUtil.masking(MaskingType.ADDRESS, "北京市海淀区马连洼街道289号"));
+ assertEquals("d*************@gmail.com.cn", MaskingUtil.masking(MaskingType.EMAIL, "duandazhi-jack@gmail.com.cn"));
+ assertEquals("**********", MaskingUtil.masking(MaskingType.PASSWORD, "1234567890"));
+ assertEquals("**********", MaskingUtil.masking(MaskingType.PASSWORD, "123"));
+ assertEquals("1101 **** **** **** 3256", MaskingUtil.masking(MaskingType.BANK_CARD, "11011111222233333256"));
+ assertEquals("6227 **** **** **** 123", MaskingUtil.masking(MaskingType.BANK_CARD, "6227880100100105123"));
+ assertEquals("192.*.*.*", MaskingUtil.masking(MaskingType.IPV4, "192.168.1.1"));
+ assertEquals("2001:*:*:*:*:*:*:*", MaskingUtil.masking(MaskingType.IPV6, "2001:0db8:86a3:08d3:1319:8a2e:0370:7344"));
+ }
- Assertions.assertEquals("0", MaskingUtil.masking("100", MaskingUtil.MaskingType.USER_ID));
- Assertions.assertEquals("段**", MaskingUtil.masking("段正淳", MaskingUtil.MaskingType.CHINESE_NAME));
- Assertions.assertEquals("5***************1X", MaskingUtil.masking("51343620000320711X", MaskingUtil.MaskingType.ID_CARD));
- Assertions.assertEquals("0915*****79", MaskingUtil.masking("09157518479", MaskingUtil.MaskingType.FIXED_PHONE));
- Assertions.assertEquals("180****1999", MaskingUtil.masking("18049531999", MaskingUtil.MaskingType.MOBILE_PHONE));
- Assertions.assertEquals("北京市海淀区马********", MaskingUtil.masking("北京市海淀区马连洼街道289号", MaskingUtil.MaskingType.ADDRESS));
- Assertions.assertEquals("d*************@gmail.com.cn", MaskingUtil.masking("duandazhi-jack@gmail.com.cn", MaskingUtil.MaskingType.EMAIL));
- Assertions.assertEquals("**********", MaskingUtil.masking("1234567890", MaskingUtil.MaskingType.PASSWORD));
- Assertions.assertEquals("1101 **** **** **** 3256", MaskingUtil.masking("11011111222233333256", MaskingUtil.MaskingType.BANK_CARD));
- Assertions.assertEquals("6227 **** **** **** 123", MaskingUtil.masking("6227880100100105123", MaskingUtil.MaskingType.BANK_CARD));
- Assertions.assertEquals("192.*.*.*", MaskingUtil.masking("192.168.1.1", MaskingUtil.MaskingType.IPV4));
- Assertions.assertEquals("2001:*:*:*:*:*:*:*", MaskingUtil.masking("2001:0db8:86a3:08d3:1319:8a2e:0370:7344", MaskingUtil.MaskingType.IPV6));
+ @Test
+ public void maskingWithMaskCharTest() {
+ final MaskingManager manager = MaskingManager.ofDefault('#');
+
+ assertEquals("", manager.masking(MaskingType.CLEAR_TO_EMPTY.name(), "100"));
+ Assertions.assertNull(manager.masking(MaskingType.CLEAR_TO_NULL.name(), "100"));
+
+ assertEquals("0", manager.masking(MaskingType.USER_ID.name(), "100"));
+ assertEquals("段##", manager.masking(MaskingType.CHINESE_NAME.name(), "段正淳"));
+ assertEquals("5###############1X", manager.masking(MaskingType.ID_CARD.name(), "51343620000320711X"));
+ assertEquals("0915#####79", manager.masking(MaskingType.FIXED_PHONE.name(), "09157518479"));
+ assertEquals("180####1999", manager.masking(MaskingType.MOBILE_PHONE.name(), "18049531999"));
+ assertEquals("北京市海淀区马########", manager.masking(MaskingType.ADDRESS.name(), "北京市海淀区马连洼街道289号"));
+ assertEquals("d#############@gmail.com.cn", manager.masking(MaskingType.EMAIL.name(), "duandazhi-jack@gmail.com.cn"));
+ assertEquals("##########", manager.masking(MaskingType.PASSWORD.name(), "1234567890"));
+ assertEquals("##########", manager.masking(MaskingType.PASSWORD.name(), "123"));
+ assertEquals("1101 #### #### #### 3256", manager.masking(MaskingType.BANK_CARD.name(), "11011111222233333256"));
+ assertEquals("6227 #### #### #### 123", manager.masking(MaskingType.BANK_CARD.name(), "6227880100100105123"));
+ assertEquals("192.#.#.#", manager.masking(MaskingType.IPV4.name(), "192.168.1.1"));
+ assertEquals("2001:#:#:#:#:#:#:#", manager.masking(MaskingType.IPV6.name(), "2001:0db8:86a3:08d3:1319:8a2e:0370:7344"));
}
@Test
public void userIdTest() {
- Assertions.assertEquals(Long.valueOf(0L), MaskingUtil.userId());
+ assertEquals(Long.valueOf(0L), MaskingUtil.userId());
}
@Test
public void chineseNameTest() {
- Assertions.assertEquals("段**", MaskingUtil.chineseName("段正淳"));
+ assertEquals("段**", MaskingUtil.chineseName("段正淳"));
}
@Test
public void idCardNumTest() {
- Assertions.assertEquals("5***************1X", MaskingUtil.idCardNum("51343620000320711X", 1, 2));
+ assertEquals("5***************1X", MaskingUtil.idCardNum("51343620000320711X", 1, 2));
}
@Test
public void fixedPhoneTest() {
- Assertions.assertEquals("0915*****79", MaskingUtil.fixedPhone("09157518479"));
+ assertEquals("0915*****79", MaskingUtil.fixedPhone("09157518479"));
}
@Test
public void mobilePhoneTest() {
- Assertions.assertEquals("180****1999", MaskingUtil.mobilePhone("18049531999"));
+ assertEquals("180****1999", MaskingUtil.mobilePhone("18049531999"));
}
@Test
public void addressTest() {
- Assertions.assertEquals("北京市海淀区马连洼街*****", MaskingUtil.address("北京市海淀区马连洼街道289号", 5));
- Assertions.assertEquals("***************", MaskingUtil.address("北京市海淀区马连洼街道289号", 50));
- Assertions.assertEquals("北京市海淀区马连洼街道289号", MaskingUtil.address("北京市海淀区马连洼街道289号", 0));
- Assertions.assertEquals("北京市海淀区马连洼街道289号", MaskingUtil.address("北京市海淀区马连洼街道289号", -1));
+ assertEquals("北京市海淀区马连洼街*****", MaskingUtil.address("北京市海淀区马连洼街道289号", 5));
+ assertEquals("***************", MaskingUtil.address("北京市海淀区马连洼街道289号", 50));
+ assertEquals("北京市海淀区马连洼街道289号", MaskingUtil.address("北京市海淀区马连洼街道289号", 0));
+ assertEquals("北京市海淀区马连洼街道289号", MaskingUtil.address("北京市海淀区马连洼街道289号", -1));
}
@Test
public void emailTest() {
- Assertions.assertEquals("d********@126.com", MaskingUtil.email("duandazhi@126.com"));
- Assertions.assertEquals("d********@gmail.com.cn", MaskingUtil.email("duandazhi@gmail.com.cn"));
- Assertions.assertEquals("d*************@gmail.com.cn", MaskingUtil.email("duandazhi-jack@gmail.com.cn"));
+ assertEquals("d********@126.com", MaskingUtil.email("duandazhi@126.com"));
+ assertEquals("d********@gmail.com.cn", MaskingUtil.email("duandazhi@gmail.com.cn"));
+ assertEquals("d*************@gmail.com.cn", MaskingUtil.email("duandazhi-jack@gmail.com.cn"));
}
@Test
public void passwordTest() {
- Assertions.assertEquals("**********", MaskingUtil.password("1234567890"));
+ assertEquals("**********", MaskingUtil.password("1234567890"));
}
@Test
public void carLicenseTest() {
- Assertions.assertEquals("", MaskingUtil.carLicense(null));
- Assertions.assertEquals("", MaskingUtil.carLicense(""));
- Assertions.assertEquals("苏D4***0", MaskingUtil.carLicense("苏D40000"));
- Assertions.assertEquals("陕A1****D", MaskingUtil.carLicense("陕A12345D"));
- Assertions.assertEquals("京A123", MaskingUtil.carLicense("京A123"));
+ assertEquals("", MaskingUtil.carLicense(null));
+ assertEquals("", MaskingUtil.carLicense(""));
+ assertEquals("苏D4***0", MaskingUtil.carLicense("苏D40000"));
+ assertEquals("陕A1****D", MaskingUtil.carLicense("陕A12345D"));
+ assertEquals("京A123", MaskingUtil.carLicense("京A123"));
}
@Test
public void bankCardTest() {
Assertions.assertNull(MaskingUtil.bankCard(null));
- Assertions.assertEquals("", MaskingUtil.bankCard(""));
- Assertions.assertEquals("1234 **** **** **** **** 9", MaskingUtil.bankCard("1234 2222 3333 4444 6789 9"));
- Assertions.assertEquals("1234 **** **** **** **** 91", MaskingUtil.bankCard("1234 2222 3333 4444 6789 91"));
- Assertions.assertEquals("1234 **** **** **** 6789", MaskingUtil.bankCard("1234 2222 3333 4444 6789"));
- Assertions.assertEquals("1234 **** **** **** 678", MaskingUtil.bankCard("1234 2222 3333 4444 678"));
+ assertEquals("", MaskingUtil.bankCard(""));
+ assertEquals("1234 **** **** **** **** 9", MaskingUtil.bankCard("1234 2222 3333 4444 6789 9"));
+ assertEquals("1234 **** **** **** **** 91", MaskingUtil.bankCard("1234 2222 3333 4444 6789 91"));
+ assertEquals("1234 **** **** **** 6789", MaskingUtil.bankCard("1234 2222 3333 4444 6789"));
+ assertEquals("1234 **** **** **** 678", MaskingUtil.bankCard("1234 2222 3333 4444 678"));
}
}