在项目开发中,数据库经常会存储大量用户敏感信息,例如手机号、身份证号、邮箱、银行卡号、家庭地址、姓名等数据。如果后端直接把原始数据返回给前端,一旦接口被抓包、日志泄露、数据库泄露,就会造成严重的用户隐私泄露问题,违反数据安全规范。
因此企业项目普遍需要做数据脱敏,也就是在不修改原始数据库数据的前提下,对返回字段进行掩码处理,隐藏部分字符,只展示明文部分。
一、脱敏方案对比
目前常见的数据脱敏实现方式有 4 种,各有优劣:
-
- 工具类手动脱敏
业务代码中手动调用工具处理,侵入性极强,每个字段都要处理,冗余代码多。
-
- AOP切面脱敏
拦截返回结果统一处理,缺点是解析整个返回体 JSON,性能较差,复杂对象嵌套难以精准控制。
-
- MyBatis 层脱敏
SQL查询、结果集映射时处理,只针对数据库查询有效,其他接口返回无法统一处理。
-
- Jackson 序列化层脱敏(本文方案)
最优方案 。基于 Jackson 自定义序列化器,在实体类字段上加注解即可生效,仅在对象转 JSON 阶段脱敏,业务零侵入、性能高、灵活可控、支持嵌套对象、全局统一,也是目前企业项目主流标准方案。
- Jackson 序列化层脱敏(本文方案)
二、脱敏核心原理
SpringBoot 默认使用 Jackson 完成 Java 对象转 JSON 字符串返回前端。
我们自定义 JsonSerializer 序列化器,实现自定义脱敏逻辑,再封装通用注解标注在实体类敏感字段上。
流程:
-
- 后端查询数据库,得到完整原始实体对象
-
- 接口返回对象,Jackson 开始序列化
-
- 检测字段上的脱敏注解
-
- 执行对应脱敏序列化器,对数据进行掩码替换
-
- 前端最终拿到脱敏后的 JSON 数据,原始数据全程不暴露
优势:原始数据库数据完全不变,仅返回时脱敏,不影响入库、不影响业务逻辑。
三、环境准备
1. Maven 依赖
SpringBoot Web 自带 Jackson,无需额外引入依赖,仅需基础 Web、Lombok 即可。
go
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-desensitization</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-desensitization</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Web 核心 内置Jackson序列化 -->
<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>
</dependencies>
</project>
2. application.yml
无需额外复杂配置,开箱即用
go
server:
port: 8080
四、统一返回结果类
沿用本专栏所有文章统一 Result 返回体,格式完全一致。
go
import lombok.Data;
@Data
public class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> error(String msg) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMsg(msg);
return result;
}
}
五、脱敏类型枚举
统一定义所有常用脱敏类型,扩展方便,规范统一。
包含:手机号、身份证、邮箱、银行卡、姓名、地址。
go
/**
* 敏感数据脱敏类型枚举
*/
public enum DesensitizeType {
/**
* 默认不处理
*/
DEFAULT,
/**
* 手机号 11位
* 138****1234
*/
PHONE,
/**
* 18位身份证
* 320123********1234
*/
ID_CARD,
/**
* 邮箱
* test****@qq.com
*/
EMAIL,
/**
* 银行卡号
* 6222****1234
*/
BANK_CARD,
/**
* 中文姓名
* 张**
*/
NAME,
/**
* 详细地址
* 广东省深圳市南山区****
*/
ADDRESS
}
六、脱敏工具类
封装所有类型脱敏算法,抽取公共工具方法,统一掩码替换规则。
go
import org.springframework.util.StringUtils;
/**
* 敏感数据脱敏工具类
*/
public class DesensitizeUtil {
/**
* 通用掩码截取方法
* @param str 原始字符串
* @param start 前面保留位数
* @param end 后面保留位数
* @return 脱敏后字符串
*/
public static String mask(String str, int start, int end) {
if (StringUtils.isEmpty(str)) {
return str;
}
int len = str.length();
// 长度不足直接返回原数据
if (len <= start + end) {
return str;
}
// 截取前缀 + 中间星号 + 截取后缀
String prefix = str.substring(0, start);
String suffix = str.substring(len - end);
return prefix + "****" + suffix;
}
// ==================== 各类敏感数据脱敏实现 ====================
/**
* 手机号脱敏 11位
*/
public static String phone(String phone) {
return mask(phone, 3, 4);
}
/**
* 身份证18位脱敏
*/
public static String idCard(String idCard) {
return mask(idCard, 6, 4);
}
/**
* 邮箱脱敏
*/
public static String email(String email) {
if (StringUtils.isEmpty(email) || !email.contains("@")) {
return email;
}
int index = email.indexOf("@");
String prefix = email.substring(0, index);
String suffix = email.substring(index);
// 用户名部分脱敏
String pre = prefix.length() > 2 ? prefix.substring(0, 2) : prefix;
return pre + "****" + suffix;
}
/**
* 银行卡脱敏
*/
public static String bankCard(String card) {
return mask(card, 4, 4);
}
/**
* 中文姓名脱敏
*/
public static String name(String name) {
if (StringUtils.isEmpty(name) || name.length() <= 1) {
return name;
}
return name.charAt(0) + "**";
}
/**
* 详细地址脱敏
*/
public static String address(String address) {
if (StringUtils.isEmpty(address) || address.length() <= 8) {
return address;
}
return address.substring(0, 8) + "****";
}
}
七、自定义脱敏注解
封装通用注解,标注在实体类字段上,指定脱敏类型,无侵入式。
go
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.*;
/**
* 自定义敏感字段脱敏注解
* 标注在实体类字段上,序列化自动脱敏
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizeSerializer.class)
public @interface Desensitize {
/**
* 脱敏类型
*/
DesensitizeType value();
}
八、自定义Jackson序列化器
实现 JsonSerializer,在序列化阶段根据注解类型,调用对应脱敏方法。
整个方案核心代码,Spring 会自动在对象转JSON时执行。
go
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
import java.io.IOException;
/**
* 自定义脱敏序列化器
* Jackson 序列化层统一处理
*/
public class DesensitizeSerializer extends StdScalarSerializer<String> {
public DesensitizeSerializer() {
super(String.class);
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// 获取字段上的脱敏注解
Desensitize annotation = gen.getAnnotation(Desensitize.class);
if (annotation == null) {
gen.writeString(value);
return;
}
DesensitizeType type = annotation.value();
String result = value;
// 根据类型执行对应脱敏算法
switch (type) {
case PHONE:
result = DesensitizeUtil.phone(value);
break;
case ID_CARD:
result = DesensitizeUtil.idCard(value);
break;
case EMAIL:
result = DesensitizeUtil.email(value);
break;
case BANK_CARD:
result = DesensitizeUtil.bankCard(value);
break;
case NAME:
result = DesensitizeUtil.name(value);
break;
case ADDRESS:
result = DesensitizeUtil.address(value);
break;
default:
break;
}
// 写入脱敏后数据
gen.writeString(result);
}
}
九、用户实体类测试
直接在敏感字段加上 @Desensitize(xxx) 注解即可,无需修改任何业务代码。
go
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private Long id;
private String username;
// 姓名脱敏
@Desensitize(DesensitizeType.NAME)
private String realName;
// 手机号脱敏
@Desensitize(DesensitizeType.PHONE)
private String phone;
// 身份证脱敏
@Desensitize(DesensitizeType.ID_CARD)
private String idCard;
// 邮箱脱敏
@Desensitize(DesensitizeType.EMAIL)
private String email;
// 银行卡脱敏
@Desensitize(DesensitizeType.BANK_CARD)
private String bankCard;
// 详细地址脱敏
@Desensitize(DesensitizeType.ADDRESS)
private String address;
}
十、测试接口
模拟数据库查询完整数据,直接返回对象,由 Jackson 序列化自动完成脱敏。
go
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/info")
public Result<User> getUserInfo() {
// 模拟数据库完整原始敏感数据
User user = new User();
user.setId(1L);
user.setUsername("admin");
user.setRealName("张三三");
user.setPhone("13812345678");
user.setIdCard("320123199801011234");
user.setEmail("zhangsan@163.com");
user.setBankCard("6222021234567891234");
user.setAddress("广东省深圳市南山区科技园高新园区1栋");
return Result.success(user);
}
}
十一、运行测试效果
启动项目访问接口:
go
http://localhost:8080/user/info
前端返回 JSON 全部自动脱敏,效果如下:
go
{
"code": 200,
"msg": "操作成功",
"data": {
"id": 1,
"username": "admin",
"realName": "张**",
"phone": "138****5678",
"idCard": "320123********1234",
"email": "zh****@163.com",
"bankCard": "6222****1234",
"address": "广东省深圳市南山区****"
}
}
数据库原始数据完全不变,仅接口返回序列化阶段脱敏。
十二、扩展增强:嵌套对象脱敏
该方案天然支持实体类嵌套对象脱敏,内部字段注解依然生效,无需额外配置。
go
@Data
class UserInfo {
@Desensitize(DesensitizeType.PHONE)
private String phone;
}
十三、生产环境扩展优化
1. 动态自定义脱敏规则
支持注解自定义前后保留位数,无需新增枚举,灵活适配各种不规则字段。
go
@Desensitize(type = CUSTOM, start = 2, end = 3)
2. 全局配置区分环境脱敏
开发、测试环境不脱敏(方便调试),生产环境自动脱敏,通过配置文件开关控制。
go
desensitize:
enable: true # 全局脱敏总开关
3. 排除指定接口脱敏
部分后台管理接口、管理员接口需要查看完整原始数据,配置放行不脱敏。
4. 结合日志脱敏
扩展实现日志打印脱敏,防止日志打印泄露用户隐私。
5. 空值、null值兼容处理
完善序列化器空值判断,避免空指针异常。
十四、方案优势总结
-
- 业务零侵入:仅实体字段加注解,Controller、Service、DAO 全部无需改动
-
- 性能极高:基于 Jackson 原生序列化,无额外反射、无JSON二次解析
-
- 统一可控:所有接口返回统一处理,一处配置全局生效
-
- 扩展方便:新增脱敏类型只需要加枚举+工具方法,无需改动核心序列化器
-
- 兼容所有返回:单对象、List集合、嵌套对象、Result包装体全部支持
-
- 数据安全:原始库数据不变,仅对外返回掩码数据,满足等保合规要求
十五、常见问题
-
- 注解不生效
原因:字段是
static修饰、字段无get方法、Lombok版本过低;解决:去掉static,保证Lombok正常生成getter。
-
- 返回null不脱敏
工具类已做空值判断,直接返回null,不会报错。
-
- 集合、List返回不生效
Jackson 序列化天然支持集合遍历序列化,完全生效无需额外处理。
-
- 自定义序列化与全局Jackson配置冲突
使用
@JacksonAnnotationsInside注解整合,不会冲突。
学习本就是一个长期积累的过程,没有捷径,唯有坚持。希望这些干货能够真正帮到你,学以致用,不断提升,在自己的领域里越走越远。喜欢本文,别忘了点赞、在看、转发,我们下期干货继续!