企业级敏感数据脱敏封装
-
- 一、应用背景与合规要求
- 二、核心应用场景
-
- [1. 用户信息展示场景](#1. 用户信息展示场景)
- [2. 金融相关场景](#2. 金融相关场景)
- [3. 设备与地址场景](#3. 设备与地址场景)
- [4. 日志与数据导出场景](#4. 日志与数据导出场景)
- [5. 自定义脱敏场景](#5. 自定义脱敏场景)
- 三、工具类实现细节与核心逻辑
-
- [1. 整体思路](#1. 整体思路)
- [2. 关键问题与解决方案](#2. 关键问题与解决方案)
- [3. 常用脱敏方法](#3. 常用脱敏方法)
- 四、工具类测试
-
- [1. 测试场景覆盖](#1. 测试场景覆盖)
- [2. 测试结果示例(完整测试结果在文末)](#2. 测试结果示例(完整测试结果在文末))
- [3. 测试结论](#3. 测试结论)
- 五、完整工具类代码
- 六、总结
大家好,我是一名后端开发工程师,在近期的项目迭代中,深刻感受到敏感数据脱敏的重要性。无论是用户的个人信息、金融相关数据,还是设备信息,稍有泄露就可能引发合规风险和用户信任危机。结合项目中遇到的脱敏需求,以及踩过的坑,现整理封装了一套可直接复用的企业级敏感数据脱敏工具类,今天分享给大家,也希望和大家互相交流、共同优化。
本文将从脱敏的 应用背景、实际应用场景、工具类实现细节、测试验证四个方面 展开说明,全程注重严谨性,避免空洞的理论,聚焦代码落地和解决实际问题,力求每一个逻辑都有对应的业务支撑,每一行代码都能直接应用到项目中去。
一、应用背景与合规要求
在数字化时代,企业的业务系统中存储着大量敏感数据,这些数据涵盖个人身份信息(姓名、身份证号、手机号)、金融信息(银行卡号、交易金额)、设备信息(MAC地址)、地理位置信息(详细地址)等。随着《网络安全法》《个人信息保护法》(PIPL)等法律法规的完善,敏感数据的合规处理已成为企业不可忽视的核心需求,这不仅可能面临监管部门的处罚,还会泄露用户隐私,损害企业品牌形象。
在实际开发中,也发现很多项目存在脱敏逻辑不统一、代码冗余、边界场景处理不足等问题。比如有的模块对手机号脱敏保留前4位,有的保留前3位;有的对身份证号脱敏逻辑不兼容15位老身份证;甚至部分脱敏逻辑未做参数校验,导致空指针异常或脱敏失效。因此,封装一套通用、严谨、可扩展的脱敏工具类,成为提升开发效率、保障数据合规的关键。
本文实现的脱敏工具类,基于Java语言开发,依赖Apache Commons Lang3工具包,涵盖11种常见敏感数据类型的脱敏处理,支持自定义脱敏规则,适配企业级项目的多样化需求,同时解决了边界场景(如空值、非法格式数据)的处理问题,确保脱敏逻辑的健壮性。
二、核心应用场景
敏感数据脱敏的核心是:在不影响业务正常开展的前提下,隐藏敏感信息的核心内容,保留必要的识别标识。结合实际项目经验,以下是工具类对应的核心应用场景,每一种场景都对应具体的业务需求,这也是开发脱敏工具类的核心依据。
1. 用户信息展示场景
在用户个人中心、后台管理系统的用户列表中,需要展示用户姓名、手机号、邮箱等信息,但需要隐藏敏感部分信息。
java
例如:
1.用户姓名"海棠朵朵"脱敏为"海**朵";
2.手机号"13812345678"脱敏为"138****5678";
3.邮箱"example@domain.com"脱敏为"exa****@domain.com",既保证用户可识别,又避免隐私泄露。
2. 金融相关场景
在金融类项目中,银行卡号、交易金额、金融价格等数据属于核心敏感信息,需要进行隐藏。
java
例如:
1.银行卡号"6222021234567890123"脱敏为"622202******90123"(保留卡BIN码和末尾4位,便于识别卡片类型);
2.交易金额"123456.78"脱敏为"1****6.78",既不泄露具体金额,又保留数据格式;
3.商品价格"129.9"脱敏为"1**.*9",适配电商、支付等场景的价格展示需求。
3. 设备与地址场景
在物联网、设备管理类项目中,MAC地址是设备的唯一标识,需脱敏处理;后台管理系统中,用户的详细地址需隐藏街道、门牌号等敏感信息,只需要保留省市区核心区域信息。
java
例如:
1.MAC地址"1A:2B:3C:4D:5E:6F"脱敏为"1A:2B:XX:XX:XX:6F";
2.详细地址"北京市东城区长安街1号院"脱敏为"北京市东城区****"。
4. 日志与数据导出场景
在系统日志、数据导出(如Excel导出用户列表)时,需对敏感数据进行脱敏,避免日志泄露或导出的 文件被非法获取后造成隐私泄露等。
java
例如:
1.日志中记录的身份证号"110101199001011234"脱敏为"110101********1234";
2.日期"2024-05-03"脱敏为"2024-**-**"(保留年份,隐藏具体月日)。
5. 自定义脱敏场景
存在不同项目的脱敏规则可能存在差异,例如有的项目需要对用户名脱敏,保留前2位、后1位,中间用*代替 等差异化。因此,在工具类中有提供通用自定义脱敏方法 ,以此来支持灵活配置前缀保留长度和后缀保留长度,适配各类个性化的脱敏需求。
三、工具类实现细节与核心逻辑
在工具类中,都采用静态方法,因此,无需实例化即可调用。核心脱敏方法包含11种、通用自定义脱敏方法1种,以及1种按敏感类型统一调用的方法,同时集成了参数校验和日志记录 ,确保代码的严谨性和可维护性。以下重点说明核心思路和关键代码细节(完整代码见文末):
1. 整体思路
工具类中的方法,每种敏感数据都对应了独立的脱敏方法,同时通过SensitiveType枚举统一管理敏感类型,方便后续进行扩展。核心如下:
- 参数校验 :对每一个输入参数进行空值校验、格式校验(如手机号必须为11位数字、MAC地址必须符合标准格式),避免空指针异常和非法数据导致的脱敏失效;
- 边界处理:针对特殊场景(如单字姓名、不足10位的银行卡号、格式不规范的地址)做特殊处理,避免过度脱敏或脱敏不彻底;
- 日志记录:通过SLF4J日志框架,记录脱敏过程中的异常情况(如空值、非法格式数据),方便问题排查;
- 可扩展性:新增脱敏类型时,只需新增枚举值和对应方法,无需修改原有代码。
2. 关键问题与解决方案
开发过程中,遇到了几个典型问题,这里整理分享出来,供大家参考:
- 金融数字脱敏正则报错问题
最初使用"[^0-9.-,]"作为正则表达式,用于过滤非数字、小数点、负号、千分位逗号以外的字符,但".-,"会被Java正则识别为"从.到,"的字符范围,导致Illegal character range异常。解决方案:将"-"移到正则表达式末尾,改为"[^0-9.,-]",解决报错问题。 - 多格式适配问题
例如MAC地址支持冒号(:)和连字符(-)两种分隔方式,地址支持"省+市+区""市+区"两种格式,车牌号支持常规车牌(共7位)和新能源车牌(共8位)。解决方案:通过正则表达式匹配多种格式,在脱敏逻辑中做兼容处理,确保不同格式的同一类数据都能正确脱敏。 - 过度脱敏问题
例如短地址(长度≤6位)、短整数(≤2位),若强行脱敏会导致数据失去识别意义。解决方案:增加长度判断,对长度不足的 data 不做脱敏处理,或保留全部内容,平衡脱敏安全性和数据可用性。
3. 常用脱敏方法
以下是几个常用的脱敏方法(完整代码见文末):
java
/**
* 姓名脱敏处理
* 规则:
* - 单字姓名: 直接返回
* - 双字姓名: 保留首字,其余用*代替
* - 三字及以上: 保留首尾字符,中间用*代替
*
* @param name 待脱敏的姓名
* @return 脱敏后的姓名
*/
public static String maskName(String name) {
if (isBlank(name)) {
LOGGER.warn("姓名脱敏处理:[{}]待脱敏姓名为空!", name);
return "";
}
int length = name.length();
if (length == 1) {
return name;
}
if (length == 2) {
return name.charAt(0) + "*";
}
return name.charAt(0) + StringUtils.repeat("*", length - 2) + name.charAt(length - 1);
}
/**
* 手机号脱敏处理
* 规则: 保留前3位和后4位,中间用4个*代替
* 示例: 13812345678 转为 138****5678
*
* @param phone 待脱敏的手机号
* @return 脱敏后的手机号(非11位手机号返回原内容)
*/
public static String maskPhone(String phone) {
if (isBlank(phone)) {
LOGGER.warn("手机号脱敏处理:[{}]待脱敏手机号为空!", phone);
return "";
}
// 移除非数字字符
String cleaned = phone.replaceAll("\\D", "");
// 校验:国内手机号通常为11位数字,非11位不脱敏,返回原内容
if (cleaned.length() != 11) {
LOGGER.info("手机号脱敏处理:非11位手机号,不做脱敏,原内容:{}", phone);
return phone;
}
return cleaned.substring(0, 3) + "****" + cleaned.substring(cleaned.length() - 4);
}
/**
* 邮箱脱敏处理
* 规则: 保留前3位和@后内容,中间用4个*代替
* 示例: example@domain.com 转为 exa****@domain.com
*
* @param email 待脱敏的邮箱
* @return 脱敏后的邮箱
*/
public static String maskEmail(String email) {
if (isBlank(email)) {
LOGGER.warn("邮箱脱敏处理:[{}]待脱敏邮箱为空!", email);
return "";
}
int atIndex = email.indexOf("@");
// 校验邮箱格式(至少包含@,且@前后有内容)
if (atIndex == -1 || atIndex == 0 || atIndex == email.length() - 1) {
LOGGER.info("邮箱脱敏处理:非合法邮箱,不做脱敏,原内容:{}", email);
return email;
}
// @符号前不足3位,不做脱敏处理
if (atIndex <= 3) {
return email;
}
return email.substring(0, 3) + "****" + email.substring(atIndex);
}
四、工具类测试
为确保工具类的正确性和健壮性,工具类同步加入了测试方法,覆盖所有脱敏方法、正常场景、边界场景(空值、非法格式、特殊长度),测试方法集成在main方法中,可直接运行验证。以下是部分测试结果:
1. 测试场景覆盖
- 正常场景:各类敏感数据格式正确,验证脱敏结果是否符合预期;
- 边界场景:空值、非法格式(如非11位手机号、非标准MAC地址)、特殊长度(单字姓名、不足10位银行卡号);
- 异常场景:含特殊字符的金融数字、多种格式的地址和日期。
2. 测试结果示例(完整测试结果在文末)
java
姓名脱敏1(海棠):海*
姓名脱敏2(海棠朵朵):海**朵
手机号脱敏1(13812345678):138****5678
06:12:38.088 [main] INFO com.learn.utils.senstive.DesensitizationUtils - 手机号脱敏处理:非11位手机号,不做脱敏,原内容:1381234
手机号脱敏2(1381234):1381234
18位身份证脱敏1(110101199001011234):110101********1234
18位身份证脱敏2(11010119900101123X):110101********123X
15位身份证脱敏1(110101900101123):110101******123
15位身份证脱敏2(11010190010112X):110101******12X
邮箱脱敏1(example@domain.com):exa****@domain.com
邮箱脱敏2(abc@163.com):abc@163.com
MAC脱敏1(1A:11:B2:C3:11:11):1A:XX:XX:XX:XX:XX
MAC脱敏2(1A-11-B2-C3-11-11):1A-XX:XX:XX:XX:XX
详细地址脱敏1(北京市东城区长安街1号院):北京市东城区******
详细地址脱敏2(上海市浦东新区博云路):上海市浦东新****
3. 测试结论
所有测试方法都已通过验证,工具类能够正确处理各类敏感数据的脱敏需求,边界场景处理合理,没有空指针异常、正则报错等问题,脱敏结果符合预期,可直接应用于企业级项目。
五、完整工具类代码
以下是完整的工具类代码,依赖Apache Commons Lang3(Maven依赖如下),可直接复制到项目中使用,如需要调整脱敏规则,可修改对应的方法:
java
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
完整工具类代码如下:
java
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 数据脱敏工具类
* 提供常见敏感信息的脱敏处理,如姓名、手机号、身份证号、邮箱等
*
*/
@Component
public class DesensitizationUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(DesensitizationUtils.class);
/**
* 姓名脱敏处理
* 规则:
* - 单字姓名: 直接返回
* - 双字姓名: 保留首字,其余用*代替
* - 三字及以上: 保留首尾字符,中间用*代替
*
* @param name 待脱敏的姓名
* @return 脱敏后的姓名
*/
public static String maskName(String name) {
if (isBlank(name)) {
LOGGER.warn("姓名脱敏处理:[{}]待脱敏姓名为空!", name);
return "";
}
int length = name.length();
if (length == 1) {
return name;
}
if (length == 2) {
return name.charAt(0) + "*";
}
return name.charAt(0) + StringUtils.repeat("*", length - 2) + name.charAt(length - 1);
}
/**
* 手机号脱敏处理
* 规则: 保留前3位和后4位,中间用4个*代替
* 示例: 13812345678 转为 138****5678
*
* @param phone 待脱敏的手机号
* @return 脱敏后的手机号(非11位手机号返回原内容)
*/
public static String maskPhone(String phone) {
if (isBlank(phone)) {
LOGGER.warn("手机号脱敏处理:[{}]待脱敏手机号为空!", phone);
return "";
}
// 移除非数字字符
String cleaned = phone.replaceAll("\\D", "");
// 校验:国内手机号通常为11位数字,非11位不脱敏,返回原内容
if (cleaned.length() != 11) {
LOGGER.info("手机号脱敏处理:非11位手机号,不做脱敏,原内容:{}", phone);
return phone;
}
return cleaned.substring(0, 3) + "****" + cleaned.substring(cleaned.length() - 4);
}
/**
* 身份证号脱敏处理
* - 15位身份证:保留前6位和后3位,中间用6个*代替
* - 18位身份证:保留前6位和后4位,中间用8个*代替
* 示例: 110101199001011234 转为 110101********1234
* 110101900101123 转为 110101******123
*
* @param idCard 待脱敏的身份证号
* @return 脱敏后的身份证号(非15/18位不做脱敏)
*/
public static String maskIdCard(String idCard) {
if (isBlank(idCard)) {
LOGGER.warn("身份证号脱敏处理:[{}]待脱敏身份证号为空!", idCard);
return "";
}
// 移除非数字和Xx字符
String cleaned = idCard.replaceAll("[^0-9Xx]", "");
int cleanedLen = cleaned.length();
// 校验:仅对15位或18位身份证号脱敏
if (cleanedLen != 15 && cleanedLen != 18) {
LOGGER.info("身份证号脱敏处理:非15/18位身份证号,不做脱敏,原内容:{}", idCard);
return idCard;
}
if (cleanedLen == 15) {
// 15位身份证:前6位+6个*+后3位
return cleaned.substring(0, 6) + "******" + cleaned.substring(cleanedLen - 3);
} else {
// 18位身份证:前6位+8个*+后4位
return cleaned.substring(0, 6) + "********" + cleaned.substring(cleanedLen - 4);
}
}
/**
* 邮箱脱敏处理
* 规则: 保留前3位和@后内容,中间用4个*代替
* 示例: example@domain.com 转为 exa****@domain.com
*
* @param email 待脱敏的邮箱
* @return 脱敏后的邮箱
*/
public static String maskEmail(String email) {
if (isBlank(email)) {
LOGGER.warn("邮箱脱敏处理:[{}]待脱敏邮箱为空!", email);
return "";
}
int atIndex = email.indexOf("@");
// 校验邮箱格式(至少包含@,且@前后有内容)
if (atIndex == -1 || atIndex == 0 || atIndex == email.length() - 1) {
LOGGER.info("邮箱脱敏处理:非合法邮箱,不做脱敏,原内容:{}", email);
return email;
}
// @符号前不足3位,不做脱敏处理
if (atIndex <= 3) {
return email;
}
return email.substring(0, 3) + "****" + email.substring(atIndex);
}
/**
* MAC地址脱敏处理
* 保留首尾两组,中间三组脱敏,兼容冒号(:)、连字符(-)分隔
* 示例:1A:2B:3C:4D:5E:6F 转为 1A:2B:XX:XX:XX:6F
* 示例:1A-2B-3C-4D-5E-6F 转为 1A-2B-XX-XX-XX-6F
*
* @param mac 待脱敏的MAC地址
* @return 脱敏后的MAC地址(非标准MAC返回原内容)
*/
public static String maskMac(String mac) {
if (isBlank(mac)) {
LOGGER.warn("MAC地址脱敏处理:[{}]待脱敏MAC为空!", mac);
return "";
}
// 正则:匹配标准MAC地址(6组2位十六进制,冒号/连字符分隔)
String macRegex = "^([0-9A-Fa-f]{2}[-:]){5}[0-9A-Fa-f]{2}$";
if (!mac.matches(macRegex)) {
LOGGER.info("MAC地址脱敏处理:非标准MAC地址,不做脱敏,原内容:{}", mac);
return mac;
}
return mac.replaceAll("([0-9A-Fa-f]{2}[-:])([0-9A-Fa-f]{2}[-:]){4}([0-9A-Fa-f]{2})",
"$1XX:XX:XX:XX:XX");
}
/**
* 详细地址脱敏处理
* 规则:保留省、市、区(前6-8位核心区域),隐藏街道、门牌号等详细信息,中间用*代替
* 适配常见地址格式,格式不规范时保留前6位,其余用*代替
* 示例:北京市东城区长安街1号院2号楼3单元 转为 北京市东城区****
* 上海市浦东新区张江高科技园区博云路 转为 上海市浦东新区****
*
* @param address 待脱敏的详细地址
* @return 脱敏后的详细地址
*/
public static String maskAddress(String address) {
if (isBlank(address)) {
LOGGER.warn("详细地址脱敏处理:[{}]待脱敏地址为空!", address);
return "";
}
// 匹配常见省市区格式(省+市+区;市+区)
String addressRegex = "([^省]+省|.+自治区|.+直辖市)([^市]+市)([^区]+区)|([^市]+市)([^区]+区)";
if (address.matches(addressRegex)) {
// 保留省市区核心部分,隐藏后续详细信息
return address.replaceAll("([^省]+省|.+自治区|.+直辖市)([^市]+市)([^区]+区).*", "$1$2$3****")
.replaceAll("([^市]+市)([^区]+区).*", "$1$2****");
}
// 地址格式不规范,保留前6位,其余用*代替(避免过度脱敏)
int length = address.length();
if (length <= 6) {
LOGGER.info("详细地址脱敏处理:地址格式不规范且长度≤6位,不做脱敏,原内容:{}", address);
return address;
}
return address.substring(0, 6) + StringUtils.repeat("*", length - 6);
}
/**
* 车牌号脱敏处理
* 规则:保留省份简称+字母(前2位),隐藏中间3位,保留最后1位,中间用*代替
* 适配国内民用车牌(含新能源车牌),非标准车牌号返回原内容
* 示例:京A12345 转为 京A***45
* 沪B67890 转为 沪B***90
* 粤A12345D(新能源)转为 粤A***45D
*
* @param licensePlate 待脱敏的车牌号
* @return 脱敏后的车牌号
*/
public static String maskLicensePlate(String licensePlate) {
if (isBlank(licensePlate)) {
LOGGER.warn("车牌号脱敏处理:[{}]待脱敏车牌号为空!", licensePlate);
return "";
}
// 正则:匹配国内车牌(常规车牌:省份简称+字母+5位字符;新能源车牌:省份简称+字母+6位字符)
String plateRegex = "^[京津沪渝冀豫云辽黑湘皖鲁闽赣粤琼甘陕吉蒙津贵川青藏琼宁新晋苏浙赣鄂桂甘晋蒙陕吉闽贵滇琼辽黑湘皖鲁粤京渝沪冀" +
"豫川云辽黑湘皖鲁闽赣粤琼甘陕吉蒙津贵川青藏琼宁新][A-Z][A-Z0-9]{5,6}$";
if (!licensePlate.matches(plateRegex)) {
LOGGER.info("车牌号脱敏处理:非标准车牌号,不做脱敏,原内容:{}", licensePlate);
return licensePlate;
}
// 区分常规车牌(7位)和新能源车牌(8位),统一脱敏逻辑
int length = licensePlate.length();
if (length == 7) {
// 常规车牌:前2位+***+最后1位
return licensePlate.substring(0, 2) + "***" + licensePlate.substring(6);
} else {
// 新能源车牌:前2位+***+最后2位(适配新能源车牌末尾多1位的格式)
return licensePlate.substring(0, 2) + "***" + licensePlate.substring(5);
}
}
/**
* 金融数字脱敏处理
* 规则:保留整数部分前1位和小数部分(若有),中间整数部分用*代替
* 适配正数、负数、带千分位的金融数字,非数字返回原内容
* 示例:123456.78 转为 1****6.78
* -98765.43 转为 -9***5.43
* 123,456.78 转为 1**,***6.78
*
* @param financialNum 待脱敏的金融数字(字符串格式)
* @return 脱敏后的金融数字
*/
public static String maskFinancialNum(String financialNum) {
if (isBlank(financialNum)) {
LOGGER.warn("金融数字脱敏处理:[{}]待脱敏金融数字为空!", financialNum);
return "";
}
// 修复报错:[^0-9.-,]正则中.-, 会被识别为字符范围(.到,),导致Illegal character range,将-移到末尾即可避免[^0-9.,-]
// 移除非数字、小数点、负号、千分位逗号以外的字符
String cleaned = financialNum.replaceAll("[^0-9.,-]", "");
// 校验:非有效金融数字(空、仅符号/小数点),返回原内容
// 同步修复匹配正则,将-移到末尾,避免同样的字符范围报错
if (isBlank(cleaned) || cleaned.matches("^[.,-]+$")) {
LOGGER.info("金融数字脱敏处理:非有效金融数字,不做脱敏,原内容:{}", financialNum);
return financialNum;
}
// 处理负号(若有)
boolean hasMinus = cleaned.startsWith("-");
String numWithoutMinus = hasMinus ? cleaned.substring(1) : cleaned;
// 区分有小数和无小数的情况
int dotIndex = numWithoutMinus.indexOf(".");
if (dotIndex != -1) {
// 有小数:整数部分保留前1位和最后1位,中间用*代替,保留小数部分
String integerPart = numWithoutMinus.substring(0, dotIndex);
String decimalPart = numWithoutMinus.substring(dotIndex);
if (integerPart.length() <= 2) {
// 整数部分≤2位,不脱敏整数部分,仅保留格式
return (hasMinus ? "-" : "") + integerPart + decimalPart;
}
String maskedInteger = integerPart.charAt(0) + StringUtils.repeat("*",
integerPart.length() - 2) + integerPart.charAt(integerPart.length() - 1);
return (hasMinus ? "-" : "") + maskedInteger + decimalPart;
} else {
// 无小数:保留前1位和最后1位,中间用*代替
if (numWithoutMinus.length() <= 2) {
return (hasMinus ? "-" : "") + numWithoutMinus;
}
String maskedNum = numWithoutMinus.charAt(0) + StringUtils.repeat("*",
numWithoutMinus.length() - 2) + numWithoutMinus.charAt(numWithoutMinus.length() - 1);
return (hasMinus ? "-" : "") + maskedNum;
}
}
/**
* 日期脱敏处理
* 规则:保留年份,隐藏月份和日期,月份日期用**代替,适配常见日期格式
* 示例:2024-05-03 转为 2024-**-**
* 2024/05/03 转为 2024/**\/**
* 2024年05月03日 转为 2024年**月**日
* @param date 待脱敏的日期(字符串格式)
* @return 脱敏后的日期
*/
public static String maskDate(String date) {
if (isBlank(date)) {
LOGGER.warn("日期脱敏处理:[{}]待脱敏日期为空!", date);
return "";
}
// 匹配常见日期格式:yyyy-MM-dd、yyyy/MM/dd、yyyy年MM月dd日、yyyy.MM.dd
String dateRegex = "^(\\d{4})([-/年.])(\\d{1,2})([-/月.])(\\d{1,2})(日?)$";
if (!date.matches(dateRegex)) {
LOGGER.info("日期脱敏处理:非标准日期格式,不做脱敏,原内容:{}", date);
return date;
}
// 保留年份和分隔符格式,替换月份和日期为**
return date.replaceAll("^(\\d{4})([-/年.])(\\d{1,2})([-/月.])(\\d{1,2})(日?)$", "$1$2**$4**$6");
}
/**
* 银行卡号脱敏处理
* 规则:保留前6位(卡BIN码)和后4位,中间用6个*代替,适配所有银行卡号长度(10-19位)
* 移除非数字字符,长度不足10位不脱敏,返回原内容
* 示例:6222021234567890123 转为 622202******90123
* 621700123456789 转为 621700******6789
*
* @param bankCard 待脱敏的银行卡号
* @return 脱敏后的银行卡号
*/
public static String maskBankCard(String bankCard) {
if (isBlank(bankCard)) {
LOGGER.warn("银行卡号脱敏处理:[{}]待脱敏银行卡号为空!", bankCard);
return "";
}
// 移除非数字字符,保留纯数字卡号
String cleaned = bankCard.replaceAll("\\D", "");
// 校验:卡号长度不足10位,不脱敏,返回原内容
if (cleaned.length() < 10) {
LOGGER.info("银行卡号脱敏处理:卡号长度不足10位,不做脱敏,原内容:{}", bankCard);
return bankCard;
}
// 保留前6位、后4位,中间用6个*代替(适配所有合法银行卡长度)
return cleaned.substring(0, 6) + "******" + cleaned.substring(cleaned.length() - 4);
}
/**
* 金融价格脱敏处理
* 规则:保留整数部分前1位,隐藏中间整数部分,保留小数部分(固定2位,不足补0)
* 适配商品价格、交易金额等场景,非数字返回原内容,自动格式化小数位
* 示例:129.9 转为 1**.*9
* 9999.99 转为 9***.99
* 5.6 转为 5*.*6
* 100 转为 1**.00
*
* @param price 待脱敏的金融价格(字符串/数字格式均可)
* @return 脱敏后的金融价格(固定2位小数)
*/
public static String maskFinancialPrice(String price) {
if (isBlank(price)) {
LOGGER.warn("金融价格脱敏处理:[{}]待脱敏金融价格为空!", price);
return "";
}
// 移除非数字、小数点以外的字符
String cleaned = price.replaceAll("[^0-9.]", "");
// 校验:非有效价格(空、仅小数点、多个小数点),返回原内容
if (isBlank(cleaned) || cleaned.matches("^\\.*$") || cleaned.indexOf(".") != cleaned.lastIndexOf(".")) {
LOGGER.info("金融价格脱敏处理:非有效金融价格,不做脱敏,原内容:{}", price);
return price;
}
// 处理小数位,固定保留2位(不足补0,多余截取)
String[] priceParts = cleaned.split("\\.");
String integerPart = priceParts[0];
String decimalPart = priceParts.length > 1 ? priceParts[1] : "00";
// 小数位处理:不足2位补0,超过2位截取前2位
decimalPart = decimalPart.length() >= 2 ? decimalPart.substring(0, 2) : StringUtils.rightPad(decimalPart, 2, "0");
// 脱敏整数部分:保留前1位,中间用*代替,整数部分仅1位则不脱敏
String maskedInteger;
if (integerPart.length() <= 1) {
maskedInteger = integerPart;
} else {
maskedInteger = integerPart.charAt(0) + StringUtils.repeat("*", integerPart.length() - 1);
}
// 拼接最终脱敏结果(固定2位小数)
return maskedInteger + "." + decimalPart;
}
/**
* 通用脱敏处理
* 规则: 保留前prefixLen位和后suffixLen位,中间用*代替
*
* @param value 待脱敏的值
* @param prefixLen 保留的前缀长度(≥0)
* @param suffixLen 保留的后缀长度(≥0)
* @return 脱敏后的值
*/
public static String maskValue(String value, int prefixLen, int suffixLen) {
if (isBlank(value)) {
LOGGER.warn("通用脱敏处理:[{}]待脱敏值为空!", value);
return "";
}
// 处理异常参数:前缀/后缀长度≤0,按0处理
prefixLen = Math.max(prefixLen, 0);
suffixLen = Math.max(suffixLen, 0);
int length = value.length();
if (length <= prefixLen + suffixLen) {
// 长度不足以分割,全部用*代替
return StringUtils.repeat("*", length);
}
String prefix = value.substring(0, prefixLen);
String suffix = value.substring(length - suffixLen);
return prefix + StringUtils.repeat("*", length - prefixLen - suffixLen) + suffix;
}
/**
* 根据敏感类型进行脱敏处理
*
* @param value 待脱敏的值
* @param type 敏感类型
* @param prefixLen 自定义脱敏时的前缀长度(仅对CUSTOM类型有效,≥0)
* @param suffixLen 自定义脱敏时的后缀长度(仅对CUSTOM类型有效,≥0)
* @return 脱敏后的值
*/
public static String maskByType(String value, SensitiveType type, int prefixLen, int suffixLen) {
if (isBlank(value)) {
LOGGER.warn("按类型脱敏处理:[{}]待脱敏值为空,敏感类型:{}", value, type);
return "";
}
switch (type) {
case NAME:
return maskName(value);
case PHONE:
return maskPhone(value);
case ID_CARD:
return maskIdCard(value);
case EMAIL:
return maskEmail(value);
case MAC:
return maskMac(value);
case ADDRESS:
return maskAddress(value);
case LICENSE_PLATE:
return maskLicensePlate(value);
case FINANCIAL_NUM:
return maskFinancialNum(value);
case DATE:
return maskDate(value);
case BANK_CARD:
return maskBankCard(value);
case FINANCIAL_PRICE:
return maskFinancialPrice(value);
case CUSTOM:
// 自定义脱敏,处理参数异常
return maskValue(value, Math.max(prefixLen, 0), Math.max(suffixLen, 0));
default:
LOGGER.info("按类型脱敏处理:未匹配到敏感类型,返回原内容:{},敏感类型:{}", value, type);
return value;
}
}
/**
* 敏感数据类型枚举
*/
public enum SensitiveType {
/** 姓名 */
NAME,
/** 手机号 */
PHONE,
/** 身份证号 */
ID_CARD,
/** 邮箱 */
EMAIL,
/** MAC地址 */
MAC,
/** 详细地址 */
ADDRESS,
/** 车牌号 */
LICENSE_PLATE,
/** 金融数字 */
FINANCIAL_NUM,
/** 日期 */
DATE,
/** 银行卡号 */
BANK_CARD,
/** 金融价格 */
FINANCIAL_PRICE,
/** 自定义 */
CUSTOM
}
/**
* 检查字符串是否为null或空
*
* @param value 待检查的字符串
* @return 是否为null或空
*/
private static boolean isBlank(String value) {
return StringUtils.isBlank(value);
}
public static void main(String[] args) {
// 姓名测试
System.out.println("姓名脱敏1(海棠):" + maskName("海棠"));
System.out.println("姓名脱敏2(海棠朵朵):" + maskName("海棠朵朵"));
// 手机号测试
System.out.println("手机号脱敏1(13812345678):" + maskPhone("13812345678"));
System.out.println("手机号脱敏2(1381234):" + maskPhone("1381234"));
// 身份证号测试
System.out.println("18位身份证脱敏1(110101199001011234):" + maskIdCard("110101199001011234"));
System.out.println("18位身份证脱敏2(11010119900101123X):" + maskIdCard("11010119900101123X"));
System.out.println("15位身份证脱敏1(110101900101123):" + maskIdCard("110101900101123"));
System.out.println("15位身份证脱敏2(11010190010112X):" + maskIdCard("11010190010112X"));
// 邮箱测试
System.out.println("邮箱脱敏1(example@domain.com):" + maskEmail("example@domain.com"));
System.out.println("邮箱脱敏2(abc@163.com):" + maskEmail("abc@163.com"));
// MAC地址测试
System.out.println("MAC脱敏1(1A:11:B2:C3:11:11):" + maskMac("1A:11:B2:C3:11:11"));
System.out.println("MAC脱敏2(1A-11-B2-C3-11-11):" + maskMac("1A-11-B2-C3-11-11"));
// 详细地址测试
System.out.println("详细地址脱敏1(北京市东城区长安街1号院):" + maskAddress("北京市东城区长安街1号院"));
System.out.println("详细地址脱敏2(上海市浦东新区博云路):" + maskAddress("上海市浦东新区博云路"));
// 车牌号测试
System.out.println("车牌号脱敏1(京A12345):" + maskLicensePlate("京A12345"));
System.out.println("车牌号脱敏2(粤A12345D):" + maskLicensePlate("粤A12345D"));
// 金融数字测试
System.out.println("金融数字脱敏1(123456.78):" + maskFinancialNum("123456.78"));
System.out.println("金融数字脱敏2(-98765.43):" + maskFinancialNum("-98765.43"));
System.out.println("金融数字脱敏3(123,456.78):" + maskFinancialNum("123,456.78"));
System.out.println("金融数字脱敏4(123.78):" + maskFinancialNum("123.78"));
System.out.println("金融数字脱敏5(12.78):" + maskFinancialNum("12.78"));
System.out.println("金融数字脱敏5(123):" + maskFinancialNum("123"));
// 日期测试
System.out.println("日期脱敏1(2026-05-03):" + maskDate("2026-05-03"));
System.out.println("日期脱敏2(2026/05/03):" + maskDate("2026/05/03"));
System.out.println("日期脱敏2(2026.05.03):" + maskDate("2026.05.03"));
System.out.println("日期脱敏3(2026年05月03日):" + maskDate("2026年05月03日"));
// 银行卡号测试
System.out.println("银行卡号脱敏1(6222021234567890123):" + maskBankCard("6222021234567890123"));
System.out.println("银行卡号脱敏2(621700123456789):" + maskBankCard("621700123456789"));
// 金融价格测试
System.out.println("金融价格脱敏1(129.9):" + maskFinancialPrice("129.9"));
System.out.println("金融价格脱敏2(9999.99):" + maskFinancialPrice("9999.99"));
System.out.println("金融价格脱敏3(5.6):" + maskFinancialPrice("5.6"));
System.out.println("金融价格脱敏4(100):" + maskFinancialPrice("100"));
// 通用自定义脱敏测试
System.out.println("自定义脱敏(海棠朵朵开花遍地走湖光山色,2,4):"
+ maskByType("海棠朵朵开花遍地走湖光山色", SensitiveType.CUSTOM, 2, 4));
}
}
工具类完整测试结果如下:
java
姓名脱敏1(海棠):海*
姓名脱敏2(海棠朵朵):海**朵
手机号脱敏1(13812345678):138****5678
10:11:01.229 [main] INFO com.zhang.utils.DesensitizationUtils - 手机号脱敏处理:非11位手机号,不做脱敏,原内容:1381234
手机号脱敏2(1381234):1381234
18位身份证脱敏1(110101199001011234):110101********1234
18位身份证脱敏2(11010119900101123X):110101********123X
15位身份证脱敏1(110101900101123):110101******123
15位身份证脱敏2(11010190010112X):110101******12X
邮箱脱敏1(example@domain.com):exa****@domain.com
邮箱脱敏2(abc@163.com):abc@163.com
MAC脱敏1(1A:11:B2:C3:11:11):1A:XX:XX:XX:XX:XX
MAC脱敏2(1A-11-B2-C3-11-11):1A-XX:XX:XX:XX:XX
详细地址脱敏1(北京市东城区长安街1号院):北京市东城区******
详细地址脱敏2(上海市浦东新区博云路):上海市浦东新****
车牌号脱敏1(京A12345):京A***5
车牌号脱敏2(粤A12345D):粤A***45D
金融数字脱敏1(123456.78):1****6.78
金融数字脱敏2(-98765.43):-9***5.43
金融数字脱敏3(123,456.78):1*****6.78
金融数字脱敏4(123.78):1*3.78
金融数字脱敏5(12.78):12.78
金融数字脱敏5(123):1*3
日期脱敏1(2026-05-03):2026-**-**
日期脱敏2(2026/05/03):2026/**/**
日期脱敏2(2026.05.03):2026.**.**
日期脱敏3(2026年05月03日):2026年**月**日
银行卡号脱敏1(6222021234567890123):622202******0123
银行卡号脱敏2(621700123456789):621700******6789
金融价格脱敏1(129.9):1**.90
金融价格脱敏2(9999.99):9***.99
金融价格脱敏3(5.6):5.60
金融价格脱敏4(100):1**.00
自定义脱敏(海棠朵朵开花遍地走湖光山色,2,4):海棠*******湖光山色
六、总结
敏感数据脱敏工具类,是基于实际项目需求封装的,涵盖了企业开发中常见的敏感数据类型,解决了脱敏逻辑不统一、边界场景处理不足等问题,经过完整测试验证,可直接复用。但技术没有终点,脱敏逻辑也需要根据不同项目的需求灵活调整,因此也希望和大家互相交流、共同优化。
在实际使用过程中,如果遇到了新的脱敏场景、发现了工具类中的bug,或者有更好的优化建议(比如新增脱敏类型、优化正则逻辑、提升性能等),欢迎在评论区留言交流,也可以私信一起探讨。
另外,需要说明的是,本工具类依赖Apache Commons Lang3工具包,主要用于简化字符串操作,若项目中未引入该依赖,可自行实现StringUtils的相关方法(如repeat、rightPad等)。同时,脱敏仅为敏感数据保护的一层防护,企业还需结合数据加密、访问控制等措施,构建完整的敏感数据保护体系 。
最后,感谢大家的阅读,希望能给大家的开发工作提供帮助,也期待和大家一起在技术的道路上不断进步、共同成长!