本文目录导读:

我将为您提供一个完整的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
这个方案提供了完整的、可扩展的数据脱敏解决方案,能够满足大多数业务场景的脱敏需求,同时保持了代码的简洁性和可维护性。