我们为什么需要脱敏化?
想象一下这样的场景:你正在开发一个电商系统,用户在“我的订单”页面需要查看自己的收货信息。如果直接把用户的真实姓名、手机号、完整地址都展示出来,会存在巨大的安全隐患。试想一下:当别人在你的快递信息上截个图拍个照,就会造成数据信息的泄露,况且公司的客服、运营人员,他们真的需要看到用户的完整手机号吗?根据“最小权限原则”,他们只需要看到部分信息用于核对即可。所以数据脱敏不是一个“可选项”,而是一个“必选项”。它的核心目标是:在不影响业务逻辑的前提下,对敏感数据进行改造,防止隐私泄露。
手动编码
通常情况下我们都是使用手动编码进行数据的脱敏化,
// UserDO.java - 从数据库查出的原始对象
public class UserDO {
private String username;
private String phone;
private String idCard;
// ... getter/setter
}
// 在Controller或Service层手动处理
@GetMapping("/user/info")
public UserInfoDTO getUserInfo() {
UserDO userDO = userService.getUserById(1L);
UserInfoDTO dto = new UserInfoDTO();
dto.setUsername(userDO.getUsername());
// 痛点来了:手动、硬编码、易出错、难维护
String phone = userDO.getPhone();
dto.setPhone(phone.substring(0, 3) + "****" + phone.substring(7));
String idCard = userDO.getIdCard();
dto.setIdCard(idCard.substring(0, 6) + "********" + idCard.substring(14));
return dto;
}
当然这种写发的弊端也很明显,如你的每个隐私信息都要进行手动编码,会导致大量的代码冗余,并且维护起来困难。在这里我是用一个优雅并且易懂的方法介绍一个新的方法:
基于Jackson注解的自动化脱敏
核心思路:、
- 定义一个脱敏注解
@Desensitization:用在DTO的字段上,标记该字段需要脱敏,并指定脱敏类型(如手机号、邮箱等)。 - 自定义一个JSON序列化器
DesensitizationSerializer:实现Jackson的JsonSerializer,在其中编写具体的脱敏逻辑。 - 将注解和序列化器关联:让
@Desensitization注解使用我们自定义的DesensitizationSerializer。 - 在DTO上使用注解:在需要脱敏的字段上,优雅地加上一个注解即可
还用代码进行讲解:
step1:创建脱敏类型枚举
首先我们定义一个枚举,来规范所有的脱敏类型,方便后续扩展:
// DesensitizationType.java
public enum DesensitizationType {
PHONE, // 手机号
ID_CARD, // 身份证
EMAIL, // 邮箱
BANK_CARD, // 银行卡
}
Step 2: 创建自定义脱敏注解
这个注解是我们的“指挥棒”,告诉框架哪个字段需要脱敏,以及怎么脱敏。
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD) // 作用在字段上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@JacksonAnnotationsInside // Jackson的元注解
@JsonSerialize(using = DesensitizationSerializer.class) // 指定使用哪个序列化器
public @interface Desensitization {
/**
* 脱敏类型
*/
DesensitizationType type();
/**
* 脱敏符号,默认为 *
*/
char maskChar() default '*';
}
Step 3: 创建自定义JSON序列化器
这是整个方案的核心“发动机”,负责执行具体的脱敏逻辑。
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
public class DesensitizationSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// 1. 获取字段上的 @Desensitization 注解
Desensitization annotation = gen.getCurrentValue().getClass()
.getDeclaredField(gen.getOutputContext().getCurrentName())
.getAnnotation(Desensitization.class);
if (annotation == null || value == null || value.isEmpty()) {
gen.writeString(value);
return;
}
// 2. 根据注解指定的类型进行脱敏
DesensitizationType type = annotation.type();
char maskChar = annotation.maskChar();
String maskedValue = applyMask(value, type, maskChar);
// 3. 将脱敏后的值写入JSON
gen.writeString(maskedValue);
}
private String applyMask(String value, DesensitizationType type, char maskChar) {
switch (type) {
case PHONE:
return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1" + String.valueOf(maskChar).repeat(4) + "$2");
case ID_CARD:
return value.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1" + String.valueOf(maskChar).repeat(8) + "$2");
case EMAIL:
int index = value.indexOf('@');
if (index <= 1) {
return value; // 邮箱名太短,不处理
}
String prefix = value.substring(0, 1);
String suffix = value.substring(index);
String middle = String.valueOf(maskChar).repeat(index - 1);
return prefix + middle + suffix;
case BANK_CARD:
return value.replaceAll("(\\d{4})\\d+(\\d{4})", "$1" + String.valueOf(maskChar).repeat(value.length() - 8) + "$2");
default:
return value;
}
}
}
Step 4: 在DTO上使用注解
万事俱备,只欠东风。现在,我们可以在DTO上像使用 @JsonProperty 一样简单地使用我们的 @Desensitization 注解了。
// UserSecurityInfoDTO.java
public class UserSecurityInfoDTO {
private Long id;
private String username;
@Desensitization(type = DesensitizationType.PHONE)
private String phone;
@Desensitization(type = DesensitizationType.EMAIL)
private String email;
@Desensitization(type = DesensitizationType.ID_CARD)
private String idCard;
// ... getter/setter
}
Step 5: 编写Controller进行测试
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/security-info")
public UserSecurityInfoDTO getSecurityInfo() {
// 模拟从数据库查出的原始数据
UserSecurityInfoDTO dto = new UserSecurityInfoDTO();
dto.setId(1L);
dto.setUsername("张三");
dto.setPhone("13812345678");
dto.setEmail("zhangsan@example.com");
dto.setIdCard("110101199001011234");
// 直接返回,无需任何手动处理!
return dto;
}
}
最终效果:
访问 http://localhost:8080/user/security-info,你将看到如下自动脱敏后的JSON:
{
"id": 1,
"username": "张三",
"phone": "138****5678",
"email": "z****@example.com",
"idCard": "110101********1234"
}
这是一个分隔符------------------------------------------------------------------------------------------------------
以下是煮啵遇到的场景,把这个博客当做自己的笔记了嘿嘿
本人预见到的脱敏场景:
页面发送请求,后端返回信息,返回的信息要脱敏展示,这里所用的是数据库加密和DTO脱敏双重加密情景
效果:
- 数据库中存储:a8f5f167f44f4964e6c998dee827110c(AES加密)
- API响应中返回:1101**********1234(脱敏)
数据库加密配置:
- !ENCRYPT
tables:
t_user:
columns:
id_card:
cipherColumn: id_card
encryptorName: common_encryptor # AES加密
phone:
cipherColumn: phone
encryptorName: common_encryptor
mail:
cipherColumn: mail
encryptorName: common_encryptor
encryptors:
common_encryptor:
type: AES
props:
aes-key-value: d6oadClrrb9A3GWo
脱敏序列化如下:
public class PhoneDesensitizationSerializer extends JsonSerializer<String> {
@Override
public void serialize(String phone, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
String phoneDesensitization = DesensitizedUtil.mobilePhone(phone);
jsonGenerator.writeString(phoneDesensitization);
}
}
public class IdCardDesensitizationSerializer extends JsonSerializer<String> {
@Override
public void serialize(String idCard, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
String idCardDesensitization = DesensitizedUtil.idCardNum(idCard, 4, 4);
jsonGenerator.writeString(idCardDesensitization);
}
}
后端接口:
@RestController
@RequiredArgsConstructor
public class UserInfoController {
private final UserService userService;
/**
* 根据用户名查询用户信息(返回脱敏数据)
*/
@GetMapping("/api/user-service/query")
public Result<UserQueryRespDTO> queryUserByUsername(@RequestParam("username") String username) {
return Results.success(userService.queryUserByUsername(username));
}
/**
* 根据用户名查询用户信息(返回完整数据,内部使用)
*/
@GetMapping("/api/user-service/actual/query")
public Result<UserQueryActualRespDTO> queryActualUserByUsername(@RequestParam("username") String username) {
return Results.success(userService.queryActualUserByUsername(username));
}
}
后端服务层:
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
@Override
public UserQueryRespDTO queryUserByUsername(String username) {
LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
.eq(UserDO::getUsername, username);
UserDO userDO = userMapper.selectOne(queryWrapper);
if (userDO == null) {
throw new ClientException("用户不存在,请检查用户名是否正确");
}
// BeanUtil转换:Entity -> DTO(脱敏处理在DTO层)
return BeanUtil.convert(userDO, UserQueryRespDTO.class);
}
}
脱敏DTO
@Data
public class UserQueryRespDTO {
private String username;
private String realName;
// 身份证号脱敏
@JsonSerialize(using = IdCardDesensitizationSerializer.class)
private String idCard;
// 手机号脱敏
@JsonSerialize(using = PhoneDesensitizationSerializer.class)
private String phone;
private String mail;
// ... 其他字段
}
向前端展示的数据
// 前端接收到的JSON响应
{
"success": true,
"data": {
"username": "testuser",
"realName": "张三",
"idCard": "1101**********1234", // 已脱敏
"phone": "138****5678", // 已脱敏
"mail": "test@example.com"
}
}
996

被折叠的 条评论
为什么被折叠?



