Spring Boot实现数据脱敏案例

wen java案例 2

本文目录导读:

Spring Boot实现数据脱敏案例

  1. 项目依赖配置
  2. 脱敏注解定义
  3. 脱敏类型枚举
  4. 脱敏工具类
  5. 自定义序列化器
  6. 实体类示例
  7. Controller 示例
  8. Service 实现
  9. Repository
  10. 全局配置(可选)
  11. 测试代码
  12. 扩展思路

我将为您提供一个完整的Spring Boot数据脱敏解决方案,涵盖常见的脱敏场景。

项目依赖配置

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- Jackson -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

脱敏注解定义

package com.example.demojpa.annotation;
import com.example.demojpa.enums.SensitiveType;
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
@JsonSerialize(using = SensitiveInfoSerializer.class)
public @interface SensitiveField {
    SensitiveType type() default SensitiveType.CUSTOM;
    // 自定义脱敏时的开始位置
    int startInclude() default 0;
    // 自定义脱敏时的结束位置
    int endExclude() default 0;
    // 替换字符
    String replacement() default "*";
}

脱敏类型枚举

package com.example.demojpa.enums;
public enum SensitiveType {
    // 中文名
    CHINESE_NAME,
    // 身份证号
    ID_CARD,
    // 手机号
    MOBILE_PHONE,
    // 座机号
    FIXED_PHONE,
    // 地址
    ADDRESS,
    // 电子邮件
    EMAIL,
    // 银行卡
    BANK_CARD,
    // 密码
    PASSWORD,
    // 车牌号
    CAR_NUMBER,
    // 自定义
    CUSTOM
}

脱敏工具类

package com.example.demojpa.util;
import com.example.demojpa.enums.SensitiveType;
import org.apache.commons.lang3.StringUtils;
public class SensitiveInfoUtils {
    /**
     * 中文姓名脱敏
     * 只保留第一个姓
     */
    public static String chineseName(String fullName) {
        if (StringUtils.isBlank(fullName)) {
            return "";
        }
        String name = StringUtils.left(fullName, 1);
        return StringUtils.rightPad(name, StringUtils.length(fullName), "*");
    }
    /**
     * 身份证号脱敏
     * 保留前3位和后4位
     */
    public static String idCardNum(String idCard) {
        if (StringUtils.isBlank(idCard)) {
            return "";
        }
        return StringUtils.left(idCard, 3).concat(StringUtils.removeStart(
            StringUtils.leftPad(StringUtils.right(idCard, 4), StringUtils.length(idCard), "*"), "***"));
    }
    /**
     * 手机号脱敏
     * 保留前3位和后4位
     */
    public static String mobilePhone(String num) {
        if (StringUtils.isBlank(num)) {
            return "";
        }
        return StringUtils.left(num, 3).concat("****").concat(StringUtils.right(num, 4));
    }
    /**
     * 固定电话脱敏
     */
    public static String fixedPhone(String num) {
        if (StringUtils.isBlank(num)) {
            return "";
        }
        return StringUtils.left(num, 3).concat("****").concat(StringUtils.right(num, 4));
    }
    /**
     * 地址脱敏
     * 保留到省市区
     */
    public static String address(String address) {
        if (StringUtils.isBlank(address)) {
            return "";
        }
        int length = address.length();
        if (length <= 6) {
            return address;
        }
        return StringUtils.rightPad(StringUtils.left(address, 6), length, "*");
    }
    /**
     * 电子邮件脱敏
     * 保留前缀第一个字母和域名
     */
    public static String email(String email) {
        if (StringUtils.isBlank(email)) {
            return "";
        }
        int index = StringUtils.indexOf(email, "@");
        if (index <= 1) {
            return email;
        }
        return StringUtils.rightPad(StringUtils.left(email, 1), index, "*")
                .concat(StringUtils.mid(email, index, StringUtils.length(email)));
    }
    /**
     * 银行卡号脱敏
     * 保留前4位和后4位
     */
    public static String bankCard(String cardNum) {
        if (StringUtils.isBlank(cardNum)) {
            return "";
        }
        return StringUtils.left(cardNum, 4).concat(StringUtils.removeStart(
            StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"), "****"));
    }
    /**
     * 密码脱敏
     */
    public static String password(String password) {
        if (StringUtils.isBlank(password)) {
            return "";
        }
        return "******";
    }
    /**
     * 车牌号脱敏
     */
    public static String carNumber(String carNumber) {
        if (StringUtils.isBlank(carNumber)) {
            return "";
        }
        return StringUtils.left(carNumber, 3).concat("****").concat(StringUtils.right(carNumber, 1));
    }
    /**
     * 自定义脱敏
     */
    public static String custom(String str, int startInclude, int endExclude, String replacement) {
        if (StringUtils.isBlank(str)) {
            return "";
        }
        if (startInclude < 0 || endExclude > str.length() || startInclude >= endExclude) {
            return str;
        }
        String replacementStr = StringUtils.repeat(replacement, endExclude - startInclude);
        return StringUtils.left(str, startInclude)
                .concat(replacementStr)
                .concat(StringUtils.substring(str, endExclude));
    }
}

自定义序列化器

package com.example.demojpa.serializer;
import com.example.demojpa.annotation.SensitiveField;
import com.example.demojpa.enums.SensitiveType;
import com.example.demojpa.util.SensitiveInfoUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import java.io.IOException;
import java.util.Objects;
public class SensitiveInfoSerializer extends JsonSerializer<String> implements ContextualSerializer {
    private SensitiveType type;
    private int startInclude;
    private int endExclude;
    private String replacement;
    public SensitiveInfoSerializer() {
    }
    public SensitiveInfoSerializer(SensitiveType type, int startInclude, int endExclude, String replacement) {
        this.type = type;
        this.startInclude = startInclude;
        this.endExclude = endExclude;
        this.replacement = replacement;
    }
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        switch (type) {
            case CHINESE_NAME:
                gen.writeString(SensitiveInfoUtils.chineseName(value));
                break;
            case ID_CARD:
                gen.writeString(SensitiveInfoUtils.idCardNum(value));
                break;
            case MOBILE_PHONE:
                gen.writeString(SensitiveInfoUtils.mobilePhone(value));
                break;
            case FIXED_PHONE:
                gen.writeString(SensitiveInfoUtils.fixedPhone(value));
                break;
            case ADDRESS:
                gen.writeString(SensitiveInfoUtils.address(value));
                break;
            case EMAIL:
                gen.writeString(SensitiveInfoUtils.email(value));
                break;
            case BANK_CARD:
                gen.writeString(SensitiveInfoUtils.bankCard(value));
                break;
            case PASSWORD:
                gen.writeString(SensitiveInfoUtils.password(value));
                break;
            case CAR_NUMBER:
                gen.writeString(SensitiveInfoUtils.carNumber(value));
                break;
            case CUSTOM:
                gen.writeString(SensitiveInfoUtils.custom(value, startInclude, endExclude, replacement));
                break;
            default:
                gen.writeString(value);
                break;
        }
    }
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        if (property != null) {
            if (Objects.equals(property.getType().getRawClass(), String.class)) {
                SensitiveField sensitiveField = property.getAnnotation(SensitiveField.class);
                if (sensitiveField == null) {
                    sensitiveField = property.getContextAnnotation(SensitiveField.class);
                }
                if (sensitiveField != null) {
                    return new SensitiveInfoSerializer(
                        sensitiveField.type(),
                        sensitiveField.startInclude(),
                        sensitiveField.endExclude(),
                        sensitiveField.replacement()
                    );
                }
            }
            return prov.findValueSerializer(property.getType(), property);
        }
        return prov.findNullValueSerializer(property);
    }
}

实体类示例

package com.example.demojpa.entity;
import com.example.demojpa.annotation.SensitiveField;
import com.example.demojpa.enums.SensitiveType;
import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "user_info")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @SensitiveField(type = SensitiveType.CHINESE_NAME)
    private String name;
    @SensitiveField(type = SensitiveType.MOBILE_PHONE)
    private String phone;
    @SensitiveField(type = SensitiveType.ID_CARD)
    private String idCard;
    @SensitiveField(type = SensitiveType.EMAIL)
    private String email;
    @SensitiveField(type = SensitiveType.ADDRESS)
    private String address;
    @SensitiveField(type = SensitiveType.PASSWORD)
    private String password;
    @SensitiveField(type = SensitiveType.BANK_CARD)
    private String bankCard;
    @SensitiveField(type = SensitiveType.CUSTOM, startInclude = 0, endExclude = 4, replacement = "#")
    private String customField;
    private Integer age;
    private LocalDateTime createTime;
}

Controller 示例

package com.example.demojpa.controller;
import com.example.demojpa.entity.User;
import com.example.demojpa.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUserById(id);
    }
    @GetMapping
    public List<User> getUsers() {
        return userService.getAllUsers();
    }
    @PostMapping
    public User createUser(@RequestBody User user) {
        return userService.saveUser(user);
    }
}

Service 实现

package com.example.demojpa.service;
import com.example.demojpa.entity.User;
import com.example.demojpa.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
    public User saveUser(User user) {
        return userRepository.save(user);
    }
}

Repository

package com.example.demojpa.repository;
import com.example.demojpa.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

全局配置(可选)

package com.example.demojpa.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setFilterProvider(new SimpleFilterProvider().setFailOnUnknownId(false));
        return mapper;
    }
}

测试代码

package com.example.demojpa.controller;
import com.example.demojpa.entity.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;
    @Test
    public void testGetUser() throws Exception {
        MvcResult result = mockMvc.perform(get("/api/users/1"))
                .andExpect(status().isOk())
                .andReturn();
        String response = result.getResponse().getContentAsString();
        System.out.println("脱敏后的用户信息: " + response);
        // 验证脱敏结果
        User user = objectMapper.readValue(response, User.class);
        // 添加断言验证脱敏后的数据
    }
}

扩展思路

动态脱敏(根据用户角色)

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicSensitive {
    SensitiveType type() default SensitiveType.CUSTOM;
    String[] roles() default {}; // 可见该字段的角色
}

配置文件驱动的脱敏

# application.yml
data:
  desensitization:
    enabled: true
    fields:
      - field: phone
        type: MOBILE_PHONE
      - field: idCard
        type: ID_CARD

这个方案提供了完整的、可扩展的数据脱敏解决方案,能够满足大多数业务场景的脱敏需求,同时保持了代码的简洁性和可维护性。

抱歉,评论功能暂时关闭!