SpringBoot + 数据脱敏策略 + 注解驱动:手机号、银行卡号返回时自动掩码
前言
在当今数据安全日益重要的时代,保护用户敏感信息已成为每个系统必须面对的挑战。特别是在接口返回数据时,如何在不影响业务逻辑的情况下,对敏感信息进行脱敏处理,是一个值得深入研究的问题。
本文将详细介绍如何使用 Spring Boot 实现基于注解驱动的数据脱敏策略,实现手机号、银行卡号等敏感信息在返回时自动掩码处理。
一、数据脱敏的重要性
1. 法规要求
- 《网络安全法》:要求网络运营者对用户个人信息进行保护
- 《个人信息保护法》:明确规定个人敏感信息的处理规则
- 《数据安全法》:要求建立数据分类分级保护制度
2. 业务需求
- 保护用户隐私:防止用户敏感信息泄露
- 符合审计要求:满足内部审计和外部监管需求
- 提升系统安全性:减少敏感信息在系统中的暴露面
- 增强用户信任:让用户对系统的数据处理更有信心
3. 常见脱敏场景
| 场景 | 敏感信息 | 脱敏要求 |
|---|---|---|
| 用户注册 | 手机号、邮箱 | 部分掩码 |
| 订单管理 | 银行卡号、身份证号 | 部分掩码 |
| 员工管理 | 薪资、联系方式 | 部分掩码 |
| 日志记录 | 用户信息、交易数据 | 全量脱敏 |
二、技术选型
| 技术 | 版本 | 用途 |
|---|---|---|
| Spring Boot | 3.2.0 | 基础框架 |
| Jackson | 2.15.0 | JSON 序列化/反序列化 |
| Spring AOP | - | 切面编程 |
| 自定义注解 | - | 标记需要脱敏的字段 |
| 反射 | - | 运行时处理字段 |
三、实现方案
1. 方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 注解 + 序列化 | 无侵入性、灵活配置 | 只处理返回数据 | 接口返回脱敏 |
| AOP 切面 | 可处理多种场景 | 性能开销较大 | 全链路脱敏 |
| 数据库层面 | 存储和查询都脱敏 | 无法恢复原始数据 | 存储脱敏 |
| 工具类 | 简单直接 | 代码重复、维护困难 | 局部脱敏 |
2. 核心实现原理
本文采用 注解 + Jackson 序列化 的方案,具体实现原理如下:
- 自定义脱敏注解:标记需要脱敏的字段
- 脱敏序列化器:实现 Jackson 的 JsonSerializer 接口
- 脱敏策略:定义不同类型的脱敏规则
- 注册序列化器:将序列化器注册到 Jackson
四、代码实现
1. 项目结构
SpringBoot-Data-Masking-Demo/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── masking/
│ │ ├── DataMaskingApplication.java
│ │ ├── annotation/
│ │ │ └── DataMasking.java
│ │ ├── serializer/
│ │ │ └── DataMaskingSerializer.java
│ │ ├── strategy/
│ │ │ ├── DataMaskingStrategy.java
│ │ │ ├── MaskingType.java
│ │ │ └── impl/
│ │ │ ├── PhoneMaskingStrategy.java
│ │ │ ├── BankCardMaskingStrategy.java
│ │ │ ├── IdCardMaskingStrategy.java
│ │ │ └── CustomMaskingStrategy.java
│ │ ├── config/
│ │ │ └── JacksonConfig.java
│ │ ├── controller/
│ │ │ └── UserController.java
│ │ └── entity/
│ │ └── User.java
│ └── resources/
│ └── application.yml
├── pom.xml
└── README.md
### 2. 核心代码实现
#### 2.1 自定义脱敏注解
```java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DataMaskingSerializer.class)
public @interface DataMasking {
/**
* 脱敏类型
*/
MaskingType type();
/**
* 自定义脱敏规则(当 type 为 CUSTOM 时使用)
*/
String rule() default "";
/**
* 前缀保留长度
*/
int prefix() default 3;
/**
* 后缀保留长度
*/
int suffix() default 4;
}
enum MaskingType {
PHONE, // 手机号脱敏
BANK_CARD, // 银行卡号脱敏
ID_CARD, // 身份证号脱敏
CUSTOM, // 自定义脱敏
EMAIL, // 邮箱脱敏
ADDRESS // 地址脱敏
}
2.2 脱敏序列化器
public class DataMaskingSerializer extends JsonSerializer<String> implements ContextualSerializer {
private DataMasking dataMasking;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
String maskedValue = maskValue(value, dataMasking);
gen.writeString(maskedValue);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
DataMasking dataMasking = property.getAnnotation(DataMasking.class);
if (dataMasking == null) {
dataMasking = property.getContextAnnotation(DataMasking.class);
}
DataMaskingSerializer serializer = new DataMaskingSerializer();
serializer.dataMasking = dataMasking;
return serializer;
}
private String maskValue(String value, DataMasking dataMasking) {
MaskingType type = dataMasking.type();
DataMaskingStrategy strategy = getStrategy(type);
return strategy.mask(value, dataMasking);
}
private DataMaskingStrategy getStrategy(MaskingType type) {
// 根据类型获取对应策略
}
}
2.3 脱敏策略接口
public interface DataMaskingStrategy {
/**
* 脱敏处理
* @param value 原始值
* @param dataMasking 脱敏注解
* @return 脱敏后的值
*/
String mask(String value, DataMasking dataMasking);
}
2.4 具体脱敏策略实现
@Component
public class PhoneMaskingStrategy implements DataMaskingStrategy {
@Override
public String mask(String value, DataMasking dataMasking) {
if (value == null || value.length() != 11) {
return value;
}
return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
}
@Component
public class BankCardMaskingStrategy implements DataMaskingStrategy {
@Override
public String mask(String value, DataMasking dataMasking) {
if (value == null || value.length() < 8) {
return value;
}
int prefix = dataMasking.prefix();
int suffix = dataMasking.suffix();
if (prefix + suffix >= value.length()) {
return value;
}
StringBuilder sb = new StringBuilder();
sb.append(value.substring(0, prefix));
sb.append("****".repeat((value.length() - prefix - suffix) / 4 + 1));
sb.append(value.substring(value.length() - suffix));
return sb.toString();
}
}
3. 实体类使用示例
@Data
public class User {
private Long id;
private String name;
@DataMasking(type = MaskingType.PHONE)
private String phone;
@DataMasking(type = MaskingType.EMAIL)
private String email;
@DataMasking(type = MaskingType.ID_CARD)
private String idCard;
@DataMasking(type = MaskingType.BANK_CARD)
private String bankCard;
@DataMasking(type = MaskingType.ADDRESS, prefix = 2, suffix = 2)
private String address;
}
4. 控制器示例
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = new User();
user.setId(id);
user.setName("张三");
user.setPhone("13812345678");
user.setEmail("zhangsan@example.com");
user.setIdCard("110101199001011234");
user.setBankCard("6222021234567890123");
user.setAddress("北京市朝阳区建国路88号");
return ResponseEntity.ok(user);
}
}
五、测试结果
原始数据
User user = new User();
user.setId(1L);
user.setName("张三");
user.setPhone("13812345678");
user.setEmail("zhangsan@example.com");
user.setIdCard("110101199001011234");
user.setBankCard("6222021234567890123");
user.setAddress("北京市朝阳区建国路88号");
脱敏后返回结果
{
"id": 1,
"name": "张三",
"phone": "138****5678",
"email": "zha****@example.com",
"idCard": "110***********1234",
"bankCard": "6222****7890123",
"address": "北京**********88号"
}
六、高级功能
1. 动态脱敏规则
- 基于环境:不同环境使用不同脱敏规则
- 基于用户角色:不同角色看到不同程度的脱敏
- 基于配置:通过配置中心动态调整脱敏规则
2. 自定义脱敏策略
@DataMasking(
type = MaskingType.CUSTOM,
rule = "3,4", // 前缀3位,后缀4位
prefix = 3,
suffix = 4
)
private String customField;
3. 嵌套对象脱敏
@Data
public class Order {
private Long id;
@DataMasking(type = MaskingType.BANK_CARD)
private String bankCard;
private User user; // User 中的脱敏注解会自动生效
}
4. 集合类型脱敏
@Data
public class UserList {
private List<@DataMasking(type = MaskingType.PHONE) String> phones;
private List<User> users; // 列表中的对象会自动脱敏
}
七、性能优化
1. 缓存优化
- 策略缓存:缓存脱敏策略实例
- 结果缓存:缓存脱敏结果,避免重复计算
- 注解缓存:缓存字段的脱敏配置
2. 序列化优化
- 懒加载:只在需要时进行脱敏
- 批量处理:批量处理脱敏操作
- 并行处理:利用多线程处理大集合
3. 内存优化
- 字符串处理:使用 StringBuilder 避免字符串拼接
- 对象池:复用脱敏策略对象
- GC 优化:减少临时对象创建
八、最佳实践
1. 脱敏策略选择
| 数据类型 | 推荐脱敏策略 | 示例 |
|---|---|---|
| 手机号 | 保留前3后4 | 138****5678 |
| 身份证号 | 保留前3后4 | 110***********1234 |
| 银行卡号 | 保留前4后3 | 6222****123 |
| 邮箱 | 保留前2后域名 | zh****@example.com |
| 地址 | 保留省市和门牌号 | 北京市朝阳区********88号 |
| 姓名 | 保留姓 | 张** |
2. 安全最佳实践
- 最小权限原则:只对必要的字段进行脱敏
- 一致性:相同类型的数据脱敏规则保持一致
- 可审计性:记录脱敏操作日志
- 定期评估:定期评估脱敏策略的有效性
3. 开发最佳实践
- 统一配置:集中管理脱敏规则
- 文档化:详细记录脱敏策略和实现
- 测试覆盖:编写脱敏功能的单元测试
- 代码审查:确保脱敏实现的正确性
九、常见问题
Q1: 如何处理 null 值?
A: 在序列化器中添加 null 检查,对 null 值直接返回,不进行脱敏处理。
Q2: 如何处理不同长度的数据?
A: 在脱敏策略中添加长度检查,对不符合长度要求的数据不进行脱敏。
Q3: 如何实现自定义脱敏规则?
A: 使用 MaskingType.CUSTOM 类型,通过 rule 属性指定自定义规则。
Q4: 如何处理嵌套对象?
A: Jackson 会自动递归处理嵌套对象中的注解,无需特殊处理。
Q5: 如何提高脱敏性能?
A: 可以:
- 缓存脱敏结果
- 使用更高效的字符串处理方法
- 避免在高频接口中过度使用脱敏
Q6: 如何实现基于角色的脱敏?
A: 可以:
- 在脱敏策略中注入用户角色信息
- 根据角色选择不同的脱敏规则
- 使用 AOP 切面实现更复杂的逻辑
十、总结
通过 Spring Boot + 注解驱动的数据脱敏方案,我们可以:
- 无侵入性:通过注解标记需要脱敏的字段,不影响业务逻辑
- 灵活性:支持多种脱敏策略和自定义规则
- 高性能:利用 Jackson 序列化机制,性能开销小
- 可扩展性:易于添加新的脱敏策略和规则
- 安全性:有效保护用户敏感信息
这种方案不仅满足了法规要求,也提升了系统的安全性和用户信任度。在实际项目中,我们可以根据具体业务需求,选择合适的脱敏策略,为用户数据提供全方位的保护。
更多技术文章,欢迎关注公众号服务端技术精选**,及时获取最新动态。**
标题:SpringBoot + 数据脱敏策略 + 注解驱动:手机号、银行卡号返回时自动掩码
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/16/1773481267367.html
公众号:服务端技术精选
- 前言
- 一、数据脱敏的重要性
- 1. 法规要求
- 2. 业务需求
- 3. 常见脱敏场景
- 二、技术选型
- 三、实现方案
- 1. 方案对比
- 2. 核心实现原理
- 四、代码实现
- 1. 项目结构
- 2.2 脱敏序列化器
- 2.3 脱敏策略接口
- 2.4 具体脱敏策略实现
- 3. 实体类使用示例
- 4. 控制器示例
- 五、测试结果
- 原始数据
- 脱敏后返回结果
- 六、高级功能
- 1. 动态脱敏规则
- 2. 自定义脱敏策略
- 3. 嵌套对象脱敏
- 4. 集合类型脱敏
- 七、性能优化
- 1. 缓存优化
- 2. 序列化优化
- 3. 内存优化
- 八、最佳实践
- 1. 脱敏策略选择
- 2. 安全最佳实践
- 3. 开发最佳实践
- 九、常见问题
- Q1: 如何处理 null 值?
- Q2: 如何处理不同长度的数据?
- Q3: 如何实现自定义脱敏规则?
- Q4: 如何处理嵌套对象?
- Q5: 如何提高脱敏性能?
- Q6: 如何实现基于角色的脱敏?
- 十、总结
评论