基于SpringBoot常用脱敏方案

`😊 @ 作者:Lion J`

`💖 @ 主页:
https://blog.csdn.net/weixin_69252724\`
`🎉 @ 主题:基于SpringBoot常用脱敏方案`
`⏱️ @ 创作时间:2025年12月10日`

数据脱敏

引言

1.1 编写的目的

在数据脱敏功能的落地过程中,开发团队往往面临"选型难、对比杂、场景多"的困惑。目前系统梳理四种主流脱敏手段(Jackson 注解式、AOP 拦截式、DTO 手动式、数据库sql脱敏)的核心机制、优缺点及适用场景,帮助开发者在安全合规、研发效率、运行性能之间做出快速且正确的技术决策,同时规避常见的明文泄露、脏数据、性能陷阱等风险,真正做到"++敏感数据看不见,业务系统照样转++"。

1.2 效果示意

脱敏方式介绍

具体的脱敏方向大致分为两种

  1. 数据库层面

  2. (应用层)返回前端之前序列化处理

数据库层面脱敏:

在数据库层面进行脱敏通常意味着在 写入数据库之前 或 获取数据库数据时 对敏感数据进行处理,++就像熟悉的MD5 密码加密++ 也是数据库脱敏的一种体现。

  • 加密:对数据进行加密处理,使其在存储时不可读。

  • 掩码:隐藏部分数据,如电话号码、身份证号等只显示部分信息。

数据库层面脱敏的优点是可以集中管理,并且通常更安全,因为敏感数据不会被未经处理就暴露给应用程序。

应用层面脱敏:

应用层脱敏是在数据从数据库抽出来,并且在发送给前端之前对其进行处理。这通常在业务逻辑层操作

  • 脱敏对象的指定字段时调用自定义的工具类完成脱敏

3.1 Jackson + 自定义序列化器

  • 概述

在将后端的对象返回前端之前, 会通过SpringMVC 的 默认Jackson 序列化器来实现数据的json化处理, 会根据字段类型查找对应的序列化器

利用这一扩展点,我们可以:

  1. 自定义一个通用脱敏序列化器(实现 JsonSerializer<String> + ContextualSerializer);

  2. 在需要脱敏的字段上只加业务注解(如 @Desensitize(type = EMAIL));

  3. 序列化阶段 Jackson 自动回调该序列化器,实时将明文替换为星号,再写入 JSON。

全程零业务代码侵入,规则集中维护,性能接近原生(序列化器被缓存),是对外 API 层最轻量、最统一的脱敏方案。

没有注解的去选择默认的序列器***(StringSerializer、IntSerializer...)***

  • 原理

BeanSerializer 拆包器

  • 拓展

这种类型是以字段为粒度加注解, 那么只要在这个对象上加了注解, 就相当于把对象的字段标记了, ++在哪调用都得走自定义的++ ++序列化++ ++逻辑++

所以, 这里扩展一个 ++打标签++(标记)可以指定字段在某个接口脱敏, 不影响该字段在其他地方的正常展示

  • 打标签的含义

++因为序列化是通过getter 方法来获取字段的,所以可以在getter上做文章++

*++addMixIn++*的本质是:把"Mixin 接口/类"上的所有注解,原封不动地嫁接给目标实体类,但运行时仍然只出现目标实体类BCompany,Mixin 本身不会被实例化也不会出现在 JSON 里。

java 复制代码
public interface CompanyDesensMixin {
    @Desensitize(type = DesensitizeType.EMAIL)
    String getEmail();
}

3.2 AOP + 标记注解

  • 概述

把"脱敏动作"从业务代码里抽出来,做成一个横切关注点;

在方法返回前端之前,立即调用统一脱敏处理器:通过反射按字段名遍历返回对象,将敏感字段就地替换为星号。 使用方式只用在目标字段上加一个自定义注解(包含脱敏类型),业务代码零变动,即可对任意复杂嵌套对象、集合、Map 完成脱敏。

该方案以"方法"为最小粒度,一次性配置即可让接口、日志、导出等多出口同时生效,但需承担 ++反射++ 遍历带来的 CPU 开销与循环引用风险

  • 原理
  • 相关代码

    java 复制代码
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Sensitive {
        SensitiveType value();
    }
    java 复制代码
    /**
     * 脱敏类型枚举
     */
    public enum SensitiveType {
        PHONE,      // 手机号
        ID_CARD,    // 身份证
        NAME,       // 姓名
        EMAIL,      // 邮箱
        BANK_CARD   // 银行卡
    }
    java 复制代码
    /**
     * 深度脱敏AOP处理器
     */
    @Aspect
    @Component
    @Slf4j
    public class DeepSensitiveAspect {
        // 定义切点:拦截Controller层所有方法
        @Pointcut("execution(* com.jing.springbootdemo.web.BasicController.user())")
        public void controllerPointcut() {}
        
        /**
         * 环绕通知:处理Controller层返回结果
         */
        @Around("controllerPointcut()")
        public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
            Object result = joinPoint.proceed();
            long start = System.nanoTime();
            Object res = processDeepSensitive(result);
            long end = System.nanoTime();
            long cost = TimeUnit.NANOSECONDS.toMillis(end - start);
            log.info("Controller 通知总耗时 = {} ms | 签名 = {}", cost, joinPoint.getSignature().toShortString());
            return res;
        }
        
        /**
         * 递归处理单个对象所有字段
         */
        private Object processObject(Object obj) {
            if (obj == null) {
                return null;
            }
            Class<?> clazz = obj.getClass();
            try {
                // 获取所有字段(包括父类)
                List<Field> fields = getAllFields(clazz);
                for (Field field : fields) {
                    field.setAccessible(true);
                    // 检查是否有脱敏注解
                    Sensitive sensitive = field.getAnnotation(Sensitive.class);
                    if (sensitive != null && field.getType() == String.class) {
                        // 处理敏感字段
                        processSensitiveField(obj, field, sensitive);
                    } else {
                        // 递归处理嵌套对象
                        processNestedField(obj, field);
                    }
                }
            } catch (Exception e) {
                log.warn("脱敏处理失败: {}", e.getMessage());
            }
            return obj;
         }
    }

这里需要写一个递归处理对象,Map,集合(反射). 如果是多字段/深度嵌套的对象, 多层反射

3.3 DTO 手动脱敏

  • 概述

没有任何注解、没有任何框架,就是最原始的 get/set 时期的做法,++最原始最灵活++

  • 代码

    java 复制代码
    public UserDTO toDTO(UserEntity entity) {
        UserDTO dto = new UserDTO();
        // 1. 普通字段
        dto.setUserName(desensitizeName(entity.getRealName()));
        // 2. 身份证号
        dto.setIdCard(StrUtil.hide(entity.getIdCard(), 1, 17));
        // 3. 手机
        dto.setMobile(DesensitizedUtil.mobilePhone(entity.getMobile()));
        // 4. 邮箱
        dto.setEmail(DesensitizedUtil.email(entity.getEmail()));
        return dto;
    }
    
    private String desensitizeName(String fullName) {
        if (fullName == null || fullName.length() < 2) return fullName;
        return fullName.charAt(0) + "*" + fullName.substring(fullName.length() - 1);
    }

这种灵活度最高,但是如果项目多处用到数据脱敏, 就要写很多重复的代码. 且及其不好维护

3.4 数据库脱敏

  • 概述

数据库脱敏是指在数据离开数据库之前,通过 SQL 内置函数、视图、存储过程或商业插件,对敏感字段进行实时变形,使返回给应用、报表或第三方接口的数据不再包含完整明文,从而防止泄露、满足合规的一种服务端级保护手段。

sql 复制代码
SELECT 
    id,
    CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone,
    CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card,
    CONCAT(LEFT(name, 1), '*', RIGHT(name, 1)) AS name,
    CONCAT(LEFT(email, 4), '****', SUBSTRING(email, LOCATE('@', email))) AS email,
    CONCAT(LEFT(bank_card, 4), '********', RIGHT(bank_card, 4)) AS bank_card
FROM users;

通过SQL层面直接脱敏, ++SQL 复杂,维护成本高,无法动态控制++

|---------------|------------------------------------------------------|----------------------------|--------------------------------------------------|------------------------------------------|
| 方式 | 层面 | 改动 | 性能 | 灵活度 |
| jackson 自定义注解 | 实现自定义的序列化器, 走原有springMVC的流程,无侵入脱敏只用加注解, 添加脱敏方式只需要加枚举 | 自定义 序列化器和 注解 | 走原生,不用拷贝,不用反射, 效率高 | 以字段为粒度, 也可以通过给某个 方法里的指定字段脱敏,灵活度高 |
| aop+标注注解 | 以反射的方式, 在脱敏的方法上添加脱敏注解 | 不修改原有的实体,只用在需要脱敏的方法上添加标记注解 | 以反射方式进行, 遇见嵌套对象, 需要将所有字段循环反射判断是否需要脱敏, 多字段对象,影响性能 | 以方法为粒度, 通过调用工具类来脱敏, 工具类来决定哪个字段要脱敏, 灵活度不高 |
| DTO 手动脱敏 | 代码业务层面, 需要脱敏的位置,添加 pojo -> dto 的对象转换 | 需要给需要转化的对象新建dto对象 | 无反射,纯手动调用脱敏工具,效率高 | 灵活度最高,但是需要大量修改原有 的代码和编写重复代码, 维护成本高 |
| 数据库层面 | 通过sql直接将数据解决按 | 需要脱敏的地方添加sql, 添加mapper | 每行通过sql的字符串截断方法, 不能走索引 | 灵活度差, 该字段就要改sql, 需要脱敏 就要去改sql, 复用性最低 |

性能测试

综上所述, Jackson 与 AOP 反射切入的方式 比较合理, 适用于当前业务需求;

下面写两个这两种类型的 Demo 来进行性能测试

  • 测试结论

执行结果

可以看到Jackson的序列化性能是AOP方式的3~4倍

安全层面

jackson方式:

  1. 无锁、无并发问题

  2. 自定义的序列化器由springWeb自己管理

aop +标注注解:

  1. 对象如果层级嵌套多/字段多,需要手动写递归,占用大量栈空间

DTO 手动脱敏:

最原始,最安全(内存全是***,无内存残留数据)

数据库层面脱敏:

将脱敏逻辑转移到 SQL 查询出口,与业务逻辑完全解耦,依赖于SQL实现是安全等级最高的实现方式。

项目场景

对于有种情况, 在整个列表页面, 邮箱这种信息不想被一眼看到, 然后详情信息又想看见. 对于后端的实现, 用户这个数据模型肯定是一份, 不可能列表 和 详情页面各一个数据模型(对象).

这边肯定优先选择可以区分接口的方案

  • 选择方案

jackson+自定义注解方式

  1. 自定义序列化处理器,springMVC 转化时(遇见自定义的注解) 直接调用自己的序列化处理器

  2. 只用在需要的脱敏字段上加注解

  3. 增加注解规则,只要更改注解工具

  4. 后期改字段名称,直接改名称

相关推荐
Q_Q19632884755 小时前
python+django/flask+vue基于机器学习的就业岗位推荐系统
spring boot·python·django·flask·node.js
没有腰的嘟嘟嘟5 小时前
从 0 到 1:我如何用 Spring Boot 3 + Redis 打造一个生产级通用幂等与防重中间件(含图解 + 代码 + 案例)
spring boot·redis·中间件·lua
Q_Q5110082855 小时前
python+django/flask+vue基于深度学习的图书推荐系统
spring boot·python·django·flask·node.js·php
阿珊和她的猫5 小时前
解析 `<form>` 标签中 `action` 和 `method` 属性的核心作用
状态模式
codealy6 小时前
Spring 事务失效的八大场景深度解析
java·spring boot·后端·spring
小坏讲微服务6 小时前
Spring Boot 4.0 新特性整合 MyBatis-Plus 完整教程
java·spring boot·后端·spring cloud·微服务·mybatis·mybatis plus
小蒜学长6 小时前
基于Spring Boot家政服务系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端
Q_Q19632884756 小时前
python+django/flask+vue爬虫的网络小说热度分析系统
spring boot·python·django·flask·node.js·php
netyeaxi6 小时前
SpringBoot:SpringBoot2.7.x如何将logback升级到1.3.x以上版本
java·spring boot·logback