Java隐私数据的脱敏化(隐藏)

我们为什么需要脱敏化?

想象一下这样的场景:你正在开发一个电商系统,用户在“我的订单”页面需要查看自己的收货信息。如果直接把用户的真实姓名、手机号、完整地址都展示出来,会存在巨大的安全隐患。试想一下:当别人在你的快递信息上截个图拍个照,就会造成数据信息的泄露,况且公司的客服、运营人员,他们真的需要看到用户的完整手机号吗?根据“最小权限原则”,他们只需要看到部分信息用于核对即可。所以数据脱敏不是一个“可选项”,而是一个“必选项”。它的核心目标是:在不影响业务逻辑的前提下,对敏感数据进行改造,防止隐私泄露。

手动编码

通常情况下我们都是使用手动编码进行数据的脱敏化,

// 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注解的自动化脱敏

核心思路:、

  1. 定义一个脱敏注解 @Desensitization:用在DTO的字段上,标记该字段需要脱敏,并指定脱敏类型(如手机号、邮箱等)。
  2. 自定义一个JSON序列化器 DesensitizationSerializer:实现Jackson的 JsonSerializer,在其中编写具体的脱敏逻辑。
  3. 将注解和序列化器关联:让 @Desensitization 注解使用我们自定义的 DesensitizationSerializer
  4. 在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"
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值