一、前言:为什么必须隐藏敏感信息?
你是否遇到过这些场景?
- 用户反馈:"我在订单页面看到了别人的手机号!"
- 安全审计报告指出:"接口返回了完整身份证号"
- 测试环境数据库被泄露,包含大量明文用户信息
根据《个人信息保护法》和《网络安全法》,手机号、身份证、银行卡号、住址等属于敏感个人信息 ,必须进行最小化展示与传输。
但很多开发者只做了"前端打码",却忽略了:
- ❌ 数据库明文存储
- ❌ 日志打印完整信息
- ❌ 接口返回未脱敏
- ❌ 管理后台直接暴露原始数据
本文将带你构建一套覆盖存储、查询、传输、日志的全链路脱敏方案,用 Spring Boot 实现安全又优雅的数据保护!
二、敏感信息分类与脱敏规则
| 敏感类型 | 示例 | 脱敏规则(通用) |
|---|---|---|
| 手机号 | 13812345678 | 138****5678 |
| 身份证号 | 11010119900307XXXX | 110101********07XXXX |
| 银行卡号 | 6222080402564890018 | 6222 **** **** 0018 |
| 姓名 | 张三丰 | 张*丰 或 *** |
| 邮箱 | mailto:zhangsan@example.com | zh******@example.com |
| 地址 | 北京市朝阳区建国路88号 | 北京市朝阳区****** |
📌 原则 :保留必要识别信息 + 隐藏关键部分
三、方案一:接口层脱敏(推荐!灵活可控)
核心思路:通过自定义注解 + Jackson 序列化器,在 JSON 输出时自动脱敏。
1. 定义脱敏类型枚举
java
public enum SensitiveType {
MOBILE, // 手机号
ID_CARD, // 身份证
BANK_CARD, // 银行卡
EMAIL, // 邮箱
NAME, // 姓名
ADDRESS // 地址
}
2. 创建脱敏注解
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
SensitiveType value();
}
3. 实现自定义序列化器
java
public class SensitiveJsonSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
if (value == null) {
gen.writeNull();
return;
}
// 从上下文获取注解(需配合自定义模块)
// 此处简化:通过字段名或传参判断,实际可通过反射获取注解
// 更佳做法见下文"进阶实现"
String masked = mask(value, getSensitiveTypeFromContext());
gen.writeString(masked);
}
private String mask(String value, SensitiveType type) {
switch (type) {
case MOBILE:
if (value.length() == 11) {
return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
break;
case ID_CARD:
if (value.length() >= 18) {
return value.replaceAll("(\\d{6})\\d{8}(\\w{4})", "$1********$2");
}
break;
case BANK_CARD:
if (value.length() >= 16) {
return value.replaceAll(".{4}(?=.{4})", "**** ");
}
break;
case EMAIL:
if (value.contains("@")) {
String[] parts = value.split("@");
String prefix = parts[0];
if (prefix.length() > 2) {
return prefix.substring(0, 2) + "******@" + parts[1];
}
}
break;
case NAME:
if (value.length() == 2) {
return value.substring(0, 1) + "*";
} else if (value.length() > 2) {
return value.substring(0, 1) + "**";
}
break;
case ADDRESS:
if (value.length() > 6) {
return value.substring(0, 6) + "******";
}
break;
}
return value;
}
// 简化:实际应通过字段元数据获取类型
private SensitiveType getSensitiveTypeFromContext() {
// 进阶实现见下文
return SensitiveType.MOBILE;
}
}
⚠️ 上述简化版无法动态识别字段类型。下面提供生产级实现。
4. 生产级方案:通过自定义 Jackson 模块 + 反射
java
// 自定义序列化器(支持动态获取注解)
public class SensitiveJsonSerializer extends JsonSerializer<String>
implements ContextualSerializer {
private SensitiveType sensitiveType;
public SensitiveJsonSerializer() {}
public SensitiveJsonSerializer(SensitiveType sensitiveType) {
this.sensitiveType = sensitiveType;
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
if (value == null) {
gen.writeNull();
return;
}
gen.writeString(mask(value, sensitiveType));
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
if (property != null) {
Sensitive annotation = property.getAnnotation(Sensitive.class);
if (annotation != null) {
return new SensitiveJsonSerializer(annotation.value());
}
}
return this;
}
// mask 方法同上...
}
5. 在实体类中使用注解
java
public class UserDTO {
private Long id;
@Sensitive(SensitiveType.NAME)
private String name;
@Sensitive(SensitiveType.MOBILE)
private String phone;
@Sensitive(SensitiveType.ID_CARD)
private String idCard;
@Sensitive(SensitiveType.EMAIL)
private String email;
// getter/setter...
}
6. 控制器返回自动脱敏
java
@GetMapping("/user/{id}")
public UserDTO getUser(@PathVariable Long id) {
User user = userService.findById(id);
UserDTO dto = convertToDTO(user);
return dto; // 返回时自动脱敏!
}
✅ 效果:
java
{
"id": 1001,
"name": "张**",
"phone": "138****5678",
"idCard": "110101********07XXXX",
"email": "zh******@example.com"
}
四、方案二:数据库存储脱敏(高安全场景)
对于极高敏感字段(如身份证、银行卡),建议加密存储:
1. 存储时加密(AES / SM4)
java
// 保存前加密
user.setIdCard(AesUtil.encrypt(rawIdCard, key));
// 查询后解密(仅限内部系统)
String realIdCard = AesUtil.decrypt(encryptedIdCard, key);
🔒 注意:加密后无法按原文查询(如"查身份证尾号为1234的用户"),需权衡。
2. 使用数据库透明加密(TDE)
- MySQL Enterprise TDE
- Oracle TDE
- 华为云/阿里云 RDS 加密
五、其他环节脱敏
1. 日志脱敏
避免 log.info("用户信息: {}", user) 打印完整对象。
方案:
- 重写
toString()方法(不推荐,影响调试) - 使用 MDC + 日志过滤器
- 最佳:日志中只打印脱敏后的 DTO
java
log.info("处理用户 [{}] 的请求", userDTO.getPhone()); // 已脱敏
2. 管理后台权限控制
- 普通客服:只能看到
138****5678 - 高级管理员:可申请查看完整信息(需审批 + 操作留痕)
六、避坑指南:常见误区
❌ 误区 1:只在前端脱敏
风险 :F12 查看网络请求,原始数据暴露
正解 :脱敏必须在服务端完成
❌ 误区 2:脱敏规则硬编码在业务逻辑中
后果 :重复代码多,难以统一修改
正解 :使用注解 + 序列化器集中管理
❌ 误区 3:测试环境使用真实用户数据
风险 :测试库泄露 = 用户信息泄露
正解 :测试数据必须脱敏或伪造
七、进阶:动态脱敏(按角色/场景)
有时需要:
- 普通用户:
138****5678 - 本人查看:
13812345678 - 客服:
138****5678 - 财务:
13812345678(需授权)
实现思路:
- 在序列化器中注入当前用户上下文(如
UserContext.getCurrentUserId()) - 判断是否为本人或有权限
- 动态决定是否脱敏
java
if (isOwner(fieldValue, currentUserId) || hasPermission()) {
gen.writeString(value); // 不脱敏
} else {
gen.writeString(mask(value, type)); // 脱敏
}
💡 需结合 Spring Security 或自定义上下文传递机制。
八、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!