标题: 数据脱敏还在手动打码?自动化方案来了!
副标题: 从手机号到身份证,全方位数据安全防护
🎬 开篇:一次数据泄露的惨痛教训
某电商平台日志泄露事件:
日志文件:
2025-01-01 10:00:00 用户下单:张三,手机:13812345678,身份证:110101199001011234
2025-01-01 10:01:00 支付成功:银行卡:6222021234567890123
被黑客获取后:
- 10万用户信息泄露
- 被用于电信诈骗
- 公司被罚款500万 💰
- 用户流失30% 📉
- 品牌信誉受损 💔
老板:为什么会这样?!
开发:我没做脱敏... 😭
改造后(脱敏):
2025-01-01 10:00:00 用户下单:张三,手机:138****5678,身份证:110101********1234
2025-01-01 10:01:00 支付成功:银行卡:622202**********123
结果:
- 日志泄露也无法还原原始数据 ✅
- 通过等保三级认证 ✅
- 用户信任度提升 ✅
教训:数据脱敏是安全的第一道防线!
🤔 什么是数据脱敏?
数据脱敏(Data Masking): 在不影响业务使用的前提下,对敏感数据进行变形处理,使其无法被还原为原始数据。
想象一下:
- 手机号: 138****5678(中间4位打码)
- 身份证: 110101********1234(中间8位打码)
- 银行卡: 622202**********123(中间10位打码)
- 邮箱: abc***@qq.com(部分字符打码)
- 姓名: 张*(姓保留,名打码)
📚 知识地图
数据脱敏解决方案
├── 🎯 脱敏场景
│ ├── 展示脱敏(前端显示)
│ ├── 日志脱敏(日志记录)
│ ├── 导出脱敏(文件导出)
│ └── 接口脱敏(API返回)
├── 🔐 脱敏类型
│ ├── 手机号脱敏
│ ├── 身份证脱敏
│ ├── 银行卡脱敏
│ ├── 邮箱脱敏
│ ├── 姓名脱敏
│ ├── 地址脱敏
│ └── IP地址脱敏
├── ⚡ 实现方案
│ ├── 工具类(手动调用)
│ ├── 注解+AOP(自动脱敏)⭐⭐⭐⭐⭐
│ ├── JSON序列化(接口返回)⭐⭐⭐⭐⭐
│ └── MyBatis插件(查询结果)⭐⭐⭐⭐
└── 🛡️ 安全防护
├── 单向加密(MD5、SHA256)
├── 可逆加密(AES)
├── 动态脱敏(按权限)
└── 审计日志
🔧 方案1:脱敏工具类
/**
* 数据脱敏工具类
*/
public class DesensitizationUtils {
/**
* 手机号脱敏
* 138****5678
*/
public static String desensitizePhone(String phone) {
if (StringUtils.isBlank(phone) || phone.length() != 11) {
return phone;
}
return phone.replaceAll("(\d{3})\d{4}(\d{4})", "$1****$2");
}
/**
* 身份证脱敏
* 110101********1234
*/
public static String desensitizeIdCard(String idCard) {
if (StringUtils.isBlank(idCard)) {
return idCard;
}
// 18位身份证
if (idCard.length() == 18) {
return idCard.replaceAll("(\d{6})\d{8}(\d{4})", "$1********$2");
}
// 15位身份证
if (idCard.length() == 15) {
return idCard.replaceAll("(\d{6})\d{6}(\d{3})", "$1******$2");
}
return idCard;
}
/**
* 银行卡脱敏
* 622202**********123
*/
public static String desensitizeBankCard(String bankCard) {
if (StringUtils.isBlank(bankCard) || bankCard.length() < 10) {
return bankCard;
}
int length = bankCard.length();
// 保留前6位和后3位
String prefix = bankCard.substring(0, 6);
String suffix = bankCard.substring(length - 3);
String middle = StringUtils.repeat("*", length - 9);
return prefix + middle + suffix;
}
/**
* 邮箱脱敏
* abc***@qq.com
*/
public static String desensitizeEmail(String email) {
if (StringUtils.isBlank(email) || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String username = parts[0];
String domain = parts[1];
// 用户名脱敏
if (username.length() <= 3) {
username = username.charAt(0) + "***";
} else {
username = username.substring(0, 3) + "***";
}
return username + "@" + domain;
}
/**
* 姓名脱敏
* 张* / 欧阳** / 司马*春
*/
public static String desensitizeName(String name) {
if (StringUtils.isBlank(name)) {
return name;
}
int length = name.length();
if (length == 1) {
return name; // 单字名不脱敏
} else if (length == 2) {
// 两字名:张*
return name.charAt(0) + "*";
} else if (length == 3) {
// 三字名:张*三
return name.charAt(0) + "*" + name.charAt(2);
} else {
// 四字名及以上:司马*春
return name.charAt(0) + StringUtils.repeat("*", length - 2) + name.charAt(length - 1);
}
}
/**
* 地址脱敏
* 北京市海淀区****
*/
public static String desensitizeAddress(String address) {
if (StringUtils.isBlank(address) || address.length() <= 6) {
return address;
}
// 保留前6位,其余打码
return address.substring(0, 6) + StringUtils.repeat("*", address.length() - 6);
}
/**
* IP地址脱敏
* 192.168.*.*
*/
public static String desensitizeIpAddress(String ip) {
if (StringUtils.isBlank(ip)) {
return ip;
}
return ip.replaceAll("(\d+\.\d+)(\.\d+\.\d+)", "$1.*.*");
}
/**
* 固定电话脱敏
* 010-****5678
*/
public static String desensitizeLandline(String landline) {
if (StringUtils.isBlank(landline)) {
return landline;
}
return landline.replaceAll("(\d{3,4}-)\d{4}(\d{4})", "$1****$2");
}
/**
* 车牌号脱敏
* 京A***78
*/
public static String desensitizeCarPlate(String carPlate) {
if (StringUtils.isBlank(carPlate) || carPlate.length() < 7) {
return carPlate;
}
return carPlate.substring(0, 3) + "***" + carPlate.substring(carPlate.length() - 2);
}
/**
* 通用脱敏(保留前后各n位)
*/
public static String desensitize(String str, int prefixLen, int suffixLen) {
if (StringUtils.isBlank(str)) {
return str;
}
int length = str.length();
if (length <= prefixLen + suffixLen) {
return StringUtils.repeat("*", length);
}
String prefix = str.substring(0, prefixLen);
String suffix = str.substring(length - suffixLen);
String middle = StringUtils.repeat("*", length - prefixLen - suffixLen);
return prefix + middle + suffix;
}
}
🎯 方案2:注解+AOP(推荐)
定义脱敏注解
/**
* 脱敏类型枚举
*/
public enum DesensitizationType {
/**
* 手机号
*/
PHONE,
/**
* 身份证
*/
ID_CARD,
/**
* 银行卡
*/
BANK_CARD,
/**
* 邮箱
*/
EMAIL,
/**
* 姓名
*/
NAME,
/**
* 地址
*/
ADDRESS,
/**
* IP地址
*/
IP_ADDRESS,
/**
* 自定义
*/
CUSTOM
}
/**
* 脱敏注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Desensitization {
/**
* 脱敏类型
*/
DesensitizationType type();
/**
* 自定义脱敏规则(type=CUSTOM时使用)
* 保留前几位
*/
int prefixLen() default 0;
/**
* 自定义脱敏规则(type=CUSTOM时使用)
* 保留后几位
*/
int suffixLen() default 0;
}
JSON序列化脱敏
/**
* 脱敏JSON序列化器
*/
public class DesensitizationSerializer extends JsonSerializer<String>
implements ContextualSerializer {
private DesensitizationType type;
private int prefixLen;
private int suffixLen;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
if (StringUtils.isBlank(value)) {
gen.writeString(value);
return;
}
String desensitized = desensitize(value);
gen.writeString(desensitized);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
if (property != null) {
Desensitization annotation = property.getAnnotation(Desensitization.class);
if (annotation != null) {
this.type = annotation.type();
this.prefixLen = annotation.prefixLen();
this.suffixLen = annotation.suffixLen();
return this;
}
}
return prov.findNullValueSerializer(property);
}
/**
* 执行脱敏
*/
private String desensitize(String value) {
switch (type) {
case PHONE:
return DesensitizationUtils.desensitizePhone(value);
case ID_CARD:
return DesensitizationUtils.desensitizeIdCard(value);
case BANK_CARD:
return DesensitizationUtils.desensitizeBankCard(value);
case EMAIL:
return DesensitizationUtils.desensitizeEmail(value);
case NAME:
return DesensitizationUtils.desensitizeName(value);
case ADDRESS:
return DesensitizationUtils.desensitizeAddress(value);
case IP_ADDRESS:
return DesensitizationUtils.desensitizeIpAddress(value);
case CUSTOM:
return DesensitizationUtils.desensitize(value, prefixLen, suffixLen);
default:
return value;
}
}
}
/**
* 使用示例
*/
@Data
public class UserVO {
private Long id;
/**
* 姓名(脱敏)
*/
@Desensitization(type = DesensitizationType.NAME)
@JsonSerialize(using = DesensitizationSerializer.class)
private String name;
/**
* 手机号(脱敏)
*/
@Desensitization(type = DesensitizationType.PHONE)
@JsonSerialize(using = DesensitizationSerializer.class)
private String phone;
/**
* 身份证(脱敏)
*/
@Desensitization(type = DesensitizationType.ID_CARD)
@JsonSerialize(using = DesensitizationSerializer.class)
private String idCard;
/**
* 邮箱(脱敏)
*/
@Desensitization(type = DesensitizationType.EMAIL)
@JsonSerialize(using = DesensitizationSerializer.class)
private String email;
/**
* 银行卡(脱敏)
*/
@Desensitization(type = DesensitizationType.BANK_CARD)
@JsonSerialize(using = DesensitizationSerializer.class)
private String bankCard;
}
/**
* Controller示例
*/
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/{id}")
public Result<UserVO> getUserInfo(@PathVariable Long id) {
UserVO user = new UserVO();
user.setId(id);
user.setName("张三");
user.setPhone("13812345678");
user.setIdCard("110101199001011234");
user.setEmail("zhangsan@qq.com");
user.setBankCard("6222021234567890123");
// ⚡ 返回时自动脱敏
return Result.success(user);
/**
* 返回的JSON:
* {
* "id": 1,
* "name": "张*",
* "phone": "138****5678",
* "idCard": "110101********1234",
* "email": "zha***@qq.com",
* "bankCard": "622202**********123"
* }
*/
}
}
📝 方案3:日志脱敏
Logback配置
<!-- logback-spring.xml -->
<configuration>
<!-- 自定义脱敏转换器 -->
<conversionRule conversionWord="desensitize"
converterClass="com.example.log.DesensitizationConverter"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %desensitize(%msg)%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
日志脱敏转换器
/**
* Logback日志脱敏转换器
*/
public class DesensitizationConverter extends MessageConverter {
/**
* 手机号正则
*/
private static final Pattern PHONE_PATTERN = Pattern.compile("1[3-9]\d{9}");
/**
* 身份证正则
*/
private static final Pattern ID_CARD_PATTERN = Pattern.compile("\d{17}[\dXx]");
/**
* 银行卡正则
*/
private static final Pattern BANK_CARD_PATTERN = Pattern.compile("\d{16,19}");
/**
* 邮箱正则
*/
private static final Pattern EMAIL_PATTERN =
Pattern.compile("\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*");
@Override
public String convert(ILoggingEvent event) {
String message = super.convert(event);
if (StringUtils.isBlank(message)) {
return message;
}
// ⚡ 依次进行各类脱敏
message = desensitizePhone(message);
message = desensitizeIdCard(message);
message = desensitizeBankCard(message);
message = desensitizeEmail(message);
return message;
}
/**
* 手机号脱敏
*/
private String desensitizePhone(String message) {
Matcher matcher = PHONE_PATTERN.matcher(message);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String phone = matcher.group();
String desensitized = DesensitizationUtils.desensitizePhone(phone);
matcher.appendReplacement(sb, desensitized);
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 身份证脱敏
*/
private String desensitizeIdCard(String message) {
Matcher matcher = ID_CARD_PATTERN.matcher(message);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String idCard = matcher.group();
String desensitized = DesensitizationUtils.desensitizeIdCard(idCard);
matcher.appendReplacement(sb, desensitized);
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 银行卡脱敏
*/
private String desensitizeBankCard(String message) {
Matcher matcher = BANK_CARD_PATTERN.matcher(message);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String bankCard = matcher.group();
// 排除手机号和身份证(避免误判)
if (bankCard.length() >= 16 && bankCard.length() <= 19) {
String desensitized = DesensitizationUtils.desensitizeBankCard(bankCard);
matcher.appendReplacement(sb, desensitized);
}
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 邮箱脱敏
*/
private String desensitizeEmail(String message) {
Matcher matcher = EMAIL_PATTERN.matcher(message);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String email = matcher.group();
String desensitized = DesensitizationUtils.desensitizeEmail(email);
matcher.appendReplacement(sb, desensitized);
}
matcher.appendTail(sb);
return sb.toString();
}
}
/**
* 使用示例
*/
@Service
@Slf4j
public class OrderService {
public void createOrder(OrderDTO dto) {
// ⚡ 日志自动脱敏
log.info("用户下单:姓名={}, 手机={}, 身份证={}",
dto.getName(), dto.getPhone(), dto.getIdCard());
/**
* 实际输出:
* 用户下单:姓名=张三, 手机=138****5678, 身份证=110101********1234
*/
}
}
🔐 方案4:数据库加密存储
/**
* AES加密工具类
*/
public class AesUtils {
/**
* 密钥(生产环境应该从配置中心获取)
*/
private static final String SECRET_KEY = "1234567890abcdef";
/**
* 加密
*/
public static String encrypt(String plainText) {
try {
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
/**
* 解密
*/
public static String decrypt(String cipherText) {
try {
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decoded = Base64.getDecoder().decode(cipherText);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("解密失败", e);
}
}
}
/**
* MyBatis类型处理器(自动加解密)
*/
@MappedTypes(String.class)
public class EncryptTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter,
JdbcType jdbcType) throws SQLException {
// ⚡ 存储时加密
String encrypted = AesUtils.encrypt(parameter);
ps.setString(i, encrypted);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String encrypted = rs.getString(columnName);
return StringUtils.isBlank(encrypted) ? null : AesUtils.decrypt(encrypted);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String encrypted = rs.getString(columnIndex);
return StringUtils.isBlank(encrypted) ? null : AesUtils.decrypt(encrypted);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String encrypted = cs.getString(columnIndex);
return StringUtils.isBlank(encrypted) ? null : AesUtils.decrypt(encrypted);
}
}
/**
* 使用示例
*/
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
/**
* ⚡ 手机号(加密存储)
*/
@TableField(typeHandler = EncryptTypeHandler.class)
private String phone;
/**
* ⚡ 身份证(加密存储)
*/
@TableField(typeHandler = EncryptTypeHandler.class)
private String idCard;
}
🎯 方案5:动态脱敏(按权限)
/**
* 权限脱敏注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitivePermission {
/**
* 需要的权限
*/
String value();
}
/**
* 脱敏AOP
*/
@Aspect
@Component
@Slf4j
public class DesensitizationAspect {
@Around("@annotation(sensitivePermission)")
public Object around(ProceedingJoinPoint pjp, SensitivePermission sensitivePermission)
throws Throwable {
// 1. 执行方法
Object result = pjp.proceed();
// 2. 判断是否有查看敏感数据的权限
boolean hasPermission = checkPermission(sensitivePermission.value());
// 3. 没有权限,进行脱敏
if (!hasPermission && result != null) {
desensitizeResult(result);
}
return result;
}
/**
* 检查权限
*/
private boolean checkPermission(String permission) {
// TODO: 从当前用户的权限中判断
// UserContext.getCurrentUser().hasPermission(permission);
return false; // 示例:假设没有权限
}
/**
* 对结果进行脱敏
*/
private void desensitizeResult(Object result) {
// 通过反射,对标记了@Desensitization注解的字段进行脱敏
Class<?> clazz = result.getClass();
for (Field field : clazz.getDeclaredFields()) {
Desensitization annotation = field.getAnnotation(Desensitization.class);
if (annotation != null) {
field.setAccessible(true);
try {
Object value = field.get(result);
if (value instanceof String) {
String desensitized = desensitize((String) value, annotation.type());
field.set(result, desensitized);
}
} catch (IllegalAccessException e) {
log.error("脱敏失败:field={}", field.getName(), e);
}
}
}
}
/**
* 执行脱敏
*/
private String desensitize(String value, DesensitizationType type) {
switch (type) {
case PHONE:
return DesensitizationUtils.desensitizePhone(value);
case ID_CARD:
return DesensitizationUtils.desensitizeIdCard(value);
case BANK_CARD:
return DesensitizationUtils.desensitizeBankCard(value);
default:
return value;
}
}
}
/**
* 使用示例
*/
@RestController
@RequestMapping("/user")
public class UserController {
/**
* ⚡ 需要"查看敏感信息"权限
*/
@GetMapping("/{id}")
@SensitivePermission("user:sensitive:view")
public Result<UserVO> getUserInfo(@PathVariable Long id) {
// 如果有权限,返回完整信息
// 如果没有权限,自动脱敏
return Result.success(userService.getById(id));
}
}
✅ 最佳实践
数据脱敏完整方案:
1️⃣ 展示层脱敏:
□ JSON序列化(@JsonSerialize)
□ 接口返回自动脱敏
□ 前端显示打码
2️⃣ 日志脱敏:
□ Logback转换器
□ 正则匹配替换
□ 实时脱敏
3️⃣ 存储层加密:
□ AES可逆加密
□ MyBatis类型处理器
□ 密钥管理(配置中心)
4️⃣ 导出脱敏:
□ Excel导出时脱敏
□ CSV导出时脱敏
□ 报表数据脱敏
5️⃣ 权限控制:
□ 按角色脱敏
□ 按权限脱敏
□ 审计日志
6️⃣ 脱敏规则:
□ 手机号:138****5678
□ 身份证:110101********1234
□ 银行卡:622202**********123
□ 邮箱:abc***@qq.com
□ 姓名:张*
7️⃣ 安全防护:
□ 禁止明文存储密码
□ 敏感日志单独存储
□ 定期审计
□ 数据备份加密
🎉 总结
数据脱敏核心要点:
1️⃣ 场景全覆盖:接口、日志、导出、展示
2️⃣ 自动化:注解+AOP,无需手动调用
3️⃣ 灵活脱敏:支持按权限动态脱敏
4️⃣ 存储安全:敏感数据加密存储
5️⃣ 合规要求:满足等保、GDPR等要求
记住:数据脱敏不是可选项,是必选项! 🔐
文档编写时间:2025年10月24日
作者:热爱数据安全的隐私工程师
版本:v1.0
愿每一条数据都安全无虞! ✨