SpringBoot 敏感数据脱敏(序列化层)

在项目开发中,数据库经常会存储大量用户敏感信息,例如手机号、身份证号、邮箱、银行卡号、家庭地址、姓名等数据。如果后端直接把原始数据返回给前端,一旦接口被抓包、日志泄露、数据库泄露,就会造成严重的用户隐私泄露问题,违反数据安全规范。

因此企业项目普遍需要做数据脱敏,也就是在不修改原始数据库数据的前提下,对返回字段进行掩码处理,隐藏部分字符,只展示明文部分。

一、脱敏方案对比

目前常见的数据脱敏实现方式有 4 种,各有优劣:

    1. 工具类手动脱敏

    业务代码中手动调用工具处理,侵入性极强,每个字段都要处理,冗余代码多。

    1. AOP切面脱敏

    拦截返回结果统一处理,缺点是解析整个返回体 JSON,性能较差,复杂对象嵌套难以精准控制。

    1. MyBatis 层脱敏

    SQL查询、结果集映射时处理,只针对数据库查询有效,其他接口返回无法统一处理。

    1. Jackson 序列化层脱敏(本文方案)
      最优方案 。基于 Jackson 自定义序列化器,在实体类字段上加注解即可生效,仅在对象转 JSON 阶段脱敏,业务零侵入、性能高、灵活可控、支持嵌套对象、全局统一,也是目前企业项目主流标准方案。

二、脱敏核心原理

SpringBoot 默认使用 Jackson 完成 Java 对象转 JSON 字符串返回前端。

我们自定义 JsonSerializer 序列化器,实现自定义脱敏逻辑,再封装通用注解标注在实体类敏感字段上。

流程:

    1. 后端查询数据库,得到完整原始实体对象
    1. 接口返回对象,Jackson 开始序列化
    1. 检测字段上的脱敏注解
    1. 执行对应脱敏序列化器,对数据进行掩码替换
    1. 前端最终拿到脱敏后的 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值兼容处理

完善序列化器空值判断,避免空指针异常。

十四、方案优势总结

    1. 业务零侵入:仅实体字段加注解,Controller、Service、DAO 全部无需改动
    1. 性能极高:基于 Jackson 原生序列化,无额外反射、无JSON二次解析
    1. 统一可控:所有接口返回统一处理,一处配置全局生效
    1. 扩展方便:新增脱敏类型只需要加枚举+工具方法,无需改动核心序列化器
    1. 兼容所有返回:单对象、List集合、嵌套对象、Result包装体全部支持
    1. 数据安全:原始库数据不变,仅对外返回掩码数据,满足等保合规要求

十五、常见问题

    1. 注解不生效

    原因:字段是 static 修饰、字段无get方法、Lombok版本过低;

    解决:去掉static,保证Lombok正常生成getter。

    1. 返回null不脱敏

    工具类已做空值判断,直接返回null,不会报错。

    1. 集合、List返回不生效

    Jackson 序列化天然支持集合遍历序列化,完全生效无需额外处理。

    1. 自定义序列化与全局Jackson配置冲突

    使用 @JacksonAnnotationsInside 注解整合,不会冲突。


学习本就是一个长期积累的过程,没有捷径,唯有坚持。希望这些干货能够真正帮到你,学以致用,不断提升,在自己的领域里越走越远。喜欢本文,别忘了点赞、在看、转发,我们下期干货继续!

相关推荐
希望永不加班2 小时前
SpringBoot 数据库索引优化:慢查询分析
java·数据库·spring boot·后端·spring
胡利光2 小时前
Harness Engineering 02|Repo Harness:让仓库对 Agent 可读
java·junit·单元测试
彩票管理中心秘书长2 小时前
MySQL数据库新建流程和字符集详细介绍
后端
geovindu2 小时前
go: Proxy Pattern
开发语言·后端·设计模式·golang·代理模式
彩票管理中心秘书长2 小时前
MySQL 用户与权限管理 (DCL) 操作命令大全
后端
langsiming2 小时前
【无标题】
java·开发语言·数据库
彩票管理中心秘书长2 小时前
MySQL 索引、事务与约束操作命令大全
后端
Rust语言中文社区2 小时前
【Rust日报】2026-04-24 Vizia 0.4 发布——纯 Rust 声明式响应式 GUI 框架
开发语言·后端·rust
weisian1512 小时前
Java并发编程--45-分布式一致性协议入门:Raft、Paxos与ZAB的核心思想
java·分布式·raft·paxos·zab