数据脱敏实战:手机号/身份证/银行卡——三行代码搞定,不侵入业务逻辑

引言

上个月安全审计,我们被点名了三个问题:

  • 订单导出接口把用户手机号明文吐给了客服系统,客服离职后导出名单卖给竞品
  • 前端页面直接展示身份证号全串,截图发朋友圈就是一次个人信息泄露
  • 日志里打印了完整的银行卡号,等保测评直接不给过

整改要求:所有出参中的敏感字段必须脱敏。第一反应是写个工具类 DesensitizeUtil.maskPhone(...),然后在几十个 Controller 里逐个调用——改了三天,漏了两处,还把一个不需要脱敏的字段误伤了(测试同事的订单列表手机号变成 138****5678 无法回显)。

后来换成注解 + Jackson 序列化器方案:在 DTO 字段上标注解 @Sensitive(type = PHONE),序列化时自动脱敏,业务代码零改动。原来的三天工作量,现在每个字段只需加一行注解,而且脱敏规则集中在枚举里统一维护。

这篇文章把我们踩过的坑和最终方案完整讲一遍:

  • 三种脱敏方案的对比(Jackson 序列化器 / MyBatis TypeHandler / AOP),各自的适用边界
  • @Sensitive 注解 + 自定义 Jackson 序列化器的完整实现(支持手机号/身份证/银行卡/姓名/邮箱等 8 种类型,支持按角色动态决定是否脱敏)
  • 数据库层脱敏(MyBatis TypeHandler)和存储加密的边界——为什么"查询要用原文"的字段不能在 DB 层脱敏

一、先搞清楚:脱敏脱的是什么

1.1 脱敏的本质:最小可用原则

脱敏不是把数据打码就完事,而是在"业务可用"和"数据安全"之间找平衡

场景需要什么脱敏策略
订单列表展示手机号能认出是自己的号保留前 3 后 4:138****5678
客服核对身份验证后四位即可保留后 4:***********5678
风控分析完整号码做关联不脱敏(但走内部权限)
日志打印排查问题能对上号保留前 3 后 4
数据导出给第三方不可还原全脱敏或哈希

脱敏的核心问题不是"怎么打码",而是"每个场景需要看到多少"。同一个手机号,在客服系统、风控系统、数据仓库里的脱敏规则可能完全不同。

1.2 常见敏感数据类型与默认脱敏规则

类型示例原文脱敏后规则
手机号13812345678138****5678保留前 3 后 4
身份证110101199001011234110***********1234保留前 3 后 4
银行卡62220202001122334456222 ***********3445保留前 4 后 4
姓名张三丰张**保留首字(少数民族姓名特殊处理)
邮箱zhangsan@example.comz******@example.com保留首字符和域名
地址北京市海淀区xx路xx号北京市海淀区******保留到区级
IP192.168.1.100192.168..保留前两段
自定义任意字符串按正则/前后保留位数支持扩展

1.3 脱敏的三个层次

按数据流转的环节,脱敏可以在三个层次做:

层次时机优点缺点
序列化层(出参脱敏)Java 对象 → JSON 时零业务入侵、注解声明式、按接口灵活控制数据库和内存里仍是明文
持久层(DB 层脱敏)查询结果映射时所有下游统一脱敏、防止误用影响所有查询方,需要原文的场景失效
存储层(加密存储)写入数据库时拖库也安全需要密钥管理,模糊查询困难

这三个层次不互斥,而是组合使用:日志和出参用序列化层脱敏;导出给低信任方的数据在 DB 层脱敏;核心敏感字段(密码、密钥)加密存储。本文重点讲序列化层(最常用、侵入最小),同时给出持久层方案作对比。


二、三种方案对比:为什么推荐 Jackson 序列化器

2.1 方案一:工具类手动调用(反面教材)

// ❌ 业务代码里到处是脱敏调用
@GetMapping("/orders")
public List<OrderVO> listOrders() {
    List<OrderVO> orders = orderService.listOrders();
    for (OrderVO order : orders) {
        order.setUserPhone(DesensitizeUtil.maskPhone(order.getUserPhone()));
        order.setIdCard(DesensitizeUtil.maskIdCard(order.getIdCard()));
        order.setBankCard(DesensitizeUtil.maskBankCard(order.getBankCard()));
        // 每个接口都要写一遍,漏一处就是事故
    }
    return orders;
}

问题:侵入业务逻辑、容易遗漏、脱敏规则散落各处。新同事加接口忘了调工具类,就是一次数据泄露。这是我们要淘汰的方案。

2.2 方案二:AOP 切面(方法返回值脱敏)

// 思路:AOP 拦截 Controller 方法,对返回值里的注解字段统一脱敏
@Aspect
@Component
public class SensitiveAspect {

    @Around("@annotation(com.example.annotation.SensitiveApi)")
    public Object desensitize(ProceedingJoinPoint pjp) throws Throwable {
        Object result = pjp.proceed();
        // 反射遍历返回值对象树,找到 @Sensitive 字段,改写值
        SensitiveFieldProcessor.process(result);
        return result;
    }
}
优点缺点
业务代码只加注解,不用调工具类反射改写对象,性能开销大(每次请求遍历对象树)
与 JSON 序列化框架无关污染原对象:脱敏后的对象如果被复用(缓存、二次序列化),数据已被破坏
嵌套对象/集合/Map 里的字段处理复杂,边界情况多
拦截点在 Controller 层,内部服务间 Feign 调用不走 AOP,漏网

最大的坑是"污染原对象":如果返回值来自本地缓存,AOP 脱敏后缓存里存的就是打码数据,下一次请求返回的数据全坏了。这种 bug 非常隐蔽。

2.3 方案三:MyBatis TypeHandler(持久层脱敏)

// 思路:查询结果从 DB 映射到 Java 对象时脱敏
@MappedTypes(String.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class PhoneDesensitizeHandler extends BaseTypeHandler<String> {

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return DesensitizeUtil.maskPhone(rs.getString(columnName));
    }
    // setNonNullParameter / 其他 getNullableResult 重载略
}

// Mapper XML 里指定
<result column="user_phone" property="userPhone" typeHandler="com.example.handler.PhoneDesensitizeHandler"/>
优点缺点
所有下游统一脱敏,覆盖面最大需要原文的场景全部失效:发短信、客服回显、风控关联都拿不到原文
DB 层兜底,不怕漏每个字段都要配置 TypeHandler,配置繁琐
写库仍是明文:只防"读",不防"拖库"
Java 对象里是脱敏值,业务逻辑里再做校验/更新会出错(把打码值写回 DB)

TypeHandler 适合数据仓库、导出服务这类"只需要脱敏值"的场景。业务库慎用——业务逻辑经常需要原文(发短信、修改手机号前的验证)。

2.4 方案四:Jackson 序列化器(推荐)

// 思路:自定义 Jackson 序列化器,序列化时读取字段上的 @Sensitive 注解并打码
// DTO 定义
public class OrderVO {
    @Sensitive(type = SensitiveType.PHONE)
    private String userPhone;      // 序列化时自动变成 138****5678

    @Sensitive(type = SensitiveType.ID_CARD)
    private String idCard;

    // 非敏感字段不受影响
    private String orderId;
}
优点缺点
零业务入侵:字段加注解即可,一行代码依赖 Jackson(Spring Boot 默认集成,微服务场景基本无缺点)
不改原对象:只在 JSON 输出时打码,内存对象完好内部 RPC 如果用非 JSON 序列化(如 Protobuf),需要另配
天然覆盖所有出参:Controller、Feign、消息序列化统一生效
支持嵌套对象、集合、Map(Jackson 递归处理)
性能开销极小:序列化本来就要做,只多一次字符串处理

为什么"不改原对象"如此关键:Jackson 是在"对象 → JSON"的输出阶段做转换,Java 对象本身不变。缓存里存的是明文对象,缓存复用、二次序列化、对象回传内部服务都不受影响。而 AOP 是直接改对象,坑就在这里。

结论:出参脱敏首选 Jackson 序列化器;数据仓库/导出场景加 TypeHandler 兜底;AOP 方案不推荐。下面重点展开 Jackson 方案的完整实现。


三、@Sensitive 注解 + Jackson 序列化器完整实现

3.1 第一步:定义敏感类型枚举

/**
 * 敏感数据类型:统一维护所有脱敏规则
 * 新增类型只需加枚举 + 对应的 mask 实现
 */
public enum SensitiveType {

    /** 手机号:保留前 3 后 4 → 138****5678 */
    PHONE {
        @Override
        public String mask(String value) {
            return SensitiveUtil.mask(value, 3, 4);
        }
    },

    /** 身份证:保留前 3 后 4 → 110***********1234 */
    ID_CARD {
        @Override
        public String mask(String value) {
            return SensitiveUtil.mask(value, 3, 4);
        }
    },

    /** 银行卡:保留前 4 后 4 → 6222***********3445 */
    BANK_CARD {
        @Override
        public String mask(String value) {
            return SensitiveUtil.mask(value, 4, 4);
        }
    },

    /** 姓名:保留首字 → 张** */
    NAME {
        @Override
        public String mask(String value) {
            if (value == null || value.length() <= 1) {
                return value;
            }
            return value.charAt(0) + "*".repeat(value.length() - 1);
        }
    },

    /** 邮箱:保留首字符和域名 → z******@example.com */
    EMAIL {
        @Override
        public String mask(String value) {
            if (value == null || !value.contains("@")) {
                return value;
            }
            int at = value.indexOf('@');
            return value.charAt(0) + "******" + value.substring(at);
        }
    },

    /** 地址:保留前 9 字符(到区级) */
    ADDRESS {
        @Override
        public String mask(String value) {
            if (value == null || value.length() <= 9) {
                return value;
            }
            return value.substring(0, 9) + "******";
        }
    };

    /** 每个类型实现自己的脱敏逻辑 */
    public abstract String mask(String value);
}

3.2 第二步:定义 @Sensitive 注解

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.*;

/**
 * 敏感数据脱敏注解:标注在 DTO 字段上,序列化时自动脱敏
 *
 * @JacksonAnnotationsInside:把本注解"打包"成 Jackson 元注解,
 *   使 @Sensitive 可以直接挂载 JsonSerialize,业务侧一个注解搞定
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {

    /** 敏感类型(决定脱敏规则) */
    SensitiveType type();

    /**
     * 角色:可选。配置后只有拥有该角色的用户能看到明文
     * 留空表示所有出参一律脱敏
     */
    String role() default "";
}

@JacksonAnnotationsInside 是这个方案的关键——它把 @Sensitive@JsonSerialize 合成一个注解,业务字段上只需要写一个 @Sensitive,不需要同时写两个注解。

3.3 第三步:实现 Jackson 序列化器

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
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;

/**
 * 脱敏序列化器:序列化时读取字段的 @Sensitive 注解并按规则打码
 *
 * 实现 ContextualSerializer 是关键:
 * Jackson 为每个字段创建序列化器实例时会调用 createContextual,
 * 在这里把字段上的 @Sensitive 注解信息"注入"当前序列化器实例
 */
public class SensitiveJsonSerializer extends JsonSerializer<String>
        implements ContextualSerializer {

    private SensitiveType sensitiveType;
    /** 脱敏豁免角色:当前用户拥有该角色时输出明文(动态脱敏) */
    private String exemptRole;

    /** Jackson 要求的无参构造 */
    public SensitiveJsonSerializer() {
    }

    public SensitiveJsonSerializer(SensitiveType type, String exemptRole) {
        this.sensitiveType = type;
        this.exemptRole = exemptRole;
    }

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider provider)
            throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }
        // 1. 当前用户是否豁免(如客服管理员角色可看明文)
        if (shouldExempt()) {
            gen.writeString(value);
            return;
        }
        // 2. 按类型脱敏后输出
        gen.writeString(sensitiveType.mask(value));
    }

    /**
     * 上下文创建:Jackson 为每个字段初始化序列化器时回调
     * 从 BeanProperty 中取出字段上的 @Sensitive 注解
     */
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property) {
        if (property == null) {
            return provider.findNullValueSerializer(null);
        }
        Sensitive annotation = property.getAnnotation(Sensitive.class);
        if (annotation != null && Objects.equals(String.class, property.getType().getRawClass())) {
            return new SensitiveJsonSerializer(annotation.type(), annotation.role());
        }
        // 字段没有 @Sensitive 注解:退回默认的 String 序列化器
        return provider.findValueSerializer(property.getType(), property);
    }

    /** 动态脱敏:检查当前登录用户是否持有豁免角色 */
    private boolean shouldExempt() {
        if (exemptRole == null || exemptRole.isEmpty()) {
            return false;   // 未配置豁免角色:一律脱敏
        }
        // 从 SecurityContext / ThreadLocal 取当前用户角色(按项目实际实现)
        Set<String> currentRoles = CurrentUserHolder.getCurrentRoles();
        return currentRoles.contains(exemptRole);
    }
}

ContextualSerializer 的作用必须理解:Jackson 的序列化器实例是按字段上下文复用的,createContextual 在初始化时把"这个字段是什么敏感类型、豁免角色是什么"注入当前实例。没有它,序列化器拿不到字段上的注解信息。

3.4 第四步:通用打码工具

/**
 * 通用打码:保留前 prefix 后 suffix 位,中间用 * 填充
 * 做长度和边界保护:短字符串不脱(避免把 "138" 脱成 "***")
 */
public class SensitiveUtil {

    public static String mask(String value, int prefix, int suffix) {
        if (value == null || value.isEmpty()) {
            return value;
        }
        int length = value.length();
        // 长度不足以保留前后缀:全打码
        if (length <= prefix + suffix) {
            return "*".repeat(length);
        }
        String head = value.substring(0, prefix);
        String tail = value.substring(length - suffix);
        String middle = "*".repeat(Math.max(length - prefix - suffix, 4));
        return head + middle + tail;
    }
}

3.5 使用效果:业务代码只需加注解

/**
 * 用户订单 VO:敏感字段加 @Sensitive,其余零改动
 */
public class OrderVO {

    private String orderId;                       // 非敏感,正常输出

    @Sensitive(type = SensitiveType.PHONE)
    private String userPhone;                     // → 138****5678

    @Sensitive(type = SensitiveType.ID_CARD)
    private String idCard;                        // → 110***********1234

    @Sensitive(type = SensitiveType.NAME)
    private String userName;                      // → 张**

    @Sensitive(type = SensitiveType.BANK_CARD)
    private String bankCard;                      // → 6222***********3445

    /** 客服主管角色可看明文手机号(动态脱敏) */
    @Sensitive(type = SensitiveType.PHONE, role = "ROLE_CS_ADMIN")
    private String contactPhone;
}

// Controller:不用改任何代码!
@GetMapping("/orders/{id}")
public OrderVO getOrder(@PathVariable String id) {
    return orderService.getOrder(id);   // 序列化时自动脱敏
}

实际输出对比:

// 脱敏前(原始对象)
{
  "orderId": "ORD20240822001",
  "userPhone": "13812345678",
  "idCard": "110101199001011234",
  "userName": "张三丰",
  "bankCard": "6222020200112233445"
}

// 脱敏后(接口返回)
{
  "orderId": "ORD20240822001",
  "userPhone": "138****5678",
  "idCard": "110***********1234",
  "userName": "张**",
  "bankCard": "6222***********3445"
}

四、进阶:日志脱敏与 DB 层兜底

4.1 日志脱敏:Logback 的替换规则

序列化层管住了出参,但日志是另一个泄漏面。两种手段:

手段一:业务侧规范——日志参数用脱敏后的值:

// ❌ 危险:明文入日志
log.info("发送短信 phone={}", user.getPhone());

// ✅ 安全:日志用脱敏值
log.info("发送短信 phone={}", SensitiveType.PHONE.mask(user.getPhone()));

手段二:Logback 正则替换——兜底拦截,不依赖业务自觉:

<!-- logback.xml:用 replace 对特定 pattern 的日志内容替换 -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <filter class="ch.qos.logback.classic.boolex.OnMarkerEvaluator"/>
    <!-- 消息 pattern 替换:11 位手机号打码 -->
    <layout>
        <pattern>%d{HH:mm:ss.SSS} %-5level %logger - %replace(%msg){'(\d{3})\d{4}(\d{4})','$1****$2'}%n</pattern>
    </layout>
</appender>

%replace(%msg){'正则','替换'} 会对每行日志内容做正则替换——11 位手机号自动打码,业务代码忘了脱敏也有兜底。身份证、银行卡同理配置多组替换规则。注意正则替换有性能成本,日志量大时只在关键 appender 上开启

4.2 MyBatis TypeHandler:导出场景的 DB 层兜底

数据导出、报表服务这类"拿到的数据还要再加工/存储"的场景,对象层面的脱敏值可能被下游二次序列化"洗掉"。这时在持久层直接脱敏:

/**
 * 手机号脱敏 TypeHandler:查询映射时打码
 * 适用于数据导出服务 / 报表库 / 给低信任下游的查询
 */
@MappedTypes(String.class)
public class PhoneMaskTypeHandler extends BaseTypeHandler<String> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
            throws SQLException {
        // 写库不脱敏(业务库原文写入,加密存储另配)
        ps.setString(i, parameter);
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return SensitiveUtil.mask(rs.getString(columnName), 3, 4);
    }

    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return SensitiveUtil.mask(rs.getString(columnIndex), 3, 4);
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return SensitiveUtil.mask(cs.getString(columnIndex), 3, 4);
    }
}
<!-- Mapper 配置:只对导出查询的映射启用 -->
<resultMap id="ExportOrderMap" type="com.example.dto.ExportOrderDTO">
    <result column="user_phone" property="userPhone"
            typeHandler="com.example.handler.PhoneMaskTypeHandler"/>
    <result column="id_card" property="idCard"
            typeHandler="com.example.handler.IdCardMaskTypeHandler"/>
</resultMap>

TypeHandler 的使用边界再强调一次:业务服务慎用,因为发短信、验证码校验、手机号修改等逻辑需要原文。它适合只消费脱敏值的下游——导出、报表、开放给第三方的数据同步。

4.3 存储加密:脱敏解决不了的"拖库"问题

要分清脱敏和加密的边界:

威胁脱敏能否防护需要的手段
出参泄露(接口/前端)✅ 序列化层脱敏
日志泄露✅ 日志替换兜底
拖库(DB 文件被盗)❌ 数据库里是明文加密存储 + 密钥管理
DBA 直接查库加密存储 + 列权限

银行卡号这类高敏字段,在脱敏之上还要加密存储:

/**
 * 敏感字段加密存储:AES-GCM,密钥从 KMS/Vault 获取
 * 常用实现:MyBatis Interceptor 对指定列加解密,或 JPA AttributeConverter
 */
@Converter
public class AesEncryptConverter implements AttributeConverter<String, String> {

    private final AesCipher cipher;   // 密钥来自 Vault/KMS,绝不硬编码

    @Override
    public String convertToDatabaseColumn(String attribute) {
        return attribute == null ? null : cipher.encrypt(attribute);
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
        return dbData == null ? null : cipher.decrypt(dbData);
    }
}

三层组合拳:加密存储(防拖库)→ 持久层脱敏(防下游滥用)→ 序列化层脱敏(防出参泄露)。大多数团队先做第三层(成本最低见效最快),再逐步补齐前两层。


五、常见问题

5.1 @Sensitive 能处理嵌套对象和 List 吗?

能。Jackson 序列化是递归的:List<OrderVO>Map<String, OrderVO>OrderVO.AddressVO 里嵌套的 @Sensitive 字段都会被各自的序列化器处理,不需要额外配置。唯一的注意点是循环引用——被双向引用的对象配合 @JsonManagedReference/@JsonBackReference 处理,与脱敏无关。

5.2 同一个字段不同接口要不同脱敏规则怎么办?

三种做法:① 动态脱敏(role 属性)@Sensitive(type=PHONE, role="ROLE_CS"),客服角色看明文,普通用户看打码;② 多视图(JsonView):不同接口用不同的 View 序列化,敏感字段在受限 View 里配置更严格的脱敏类型;③ 拆 VO:风控专用的 VO 不加注解走内部权限,对外的 VO 加注解。方案①最简单,优先考虑。

5.3 序列化器会影响性能吗?

开销极小。Jackson 序列化本来就要执行,@Sensitive 只是在写 String 前多做一次 substring + 拼接,单次纳秒级。我们的订单列表接口(单次 100 条记录 × 5 个敏感字段)压测 P99 增幅 < 0.5ms。真正要注意的反而是 AOP 方案——反射遍历对象树的开销是它的几十倍。

5.4 入参需要脱敏吗?

入参"脱敏"通常指两类需求:① 日志脱敏:入参打印日志时打码,用 logback replace 或手动 mask(见 4.1);② 入参回显:表单校验失败后回显打码值,同样走出参序列化。入参本身不能改——业务逻辑需要原文(发短信、存库),改了入参业务就错了。序列化器只动出参,天然安全。

5.5 为什么不直接在 Service 层把数据改成脱敏值?

因为内存里的明文对象还有别的用途:本地缓存复用、发短信、跨服务传输、二次计算。Service 层改值会把"展示层的脱敏需求"泄漏进业务层,导致所有下游都只能拿到打码数据,还可能把打码值写回数据库造成数据污染。脱敏是展示关切,不是业务关切,应该在序列化边界处理。

5.6 微服务间 Feign 调用会被脱敏吗?

会被脱敏——Feign 默认用 Jackson 编码请求体,带 @Sensitive 的字段在调用方出参时已打码,下游收到的就是脱敏值。这通常是期望行为(最小数据原则),但如果有内部服务需要原文,用 @JsonView 区分:对外接口挂脱敏 View,内部 Feign 接口挂原文 View。


六、总结

三种方案速查卡

┌─────────────────┬──────────────┬────────────────────────────────┐
│ 方案             │ 侵入性        │ 适用场景                        │
├─────────────────┼──────────────┼────────────────────────────────┤
│ Jackson 序列化器 │ 零(加注解)   │ 出参脱敏首选,覆盖所有 JSON 序列化│
│ MyBatis TypeHandler│ 配置繁琐     │ 数据导出/报表/给低信任下游的查询 │
│ AOP 切面         │ 中(注解)     │ 不推荐:污染原对象 + 反射性能差  │
│ 工具类手动调用    │ 高(改业务码)  │ 淘汰:易遗漏,规则散落          │
└─────────────────┴──────────────┴────────────────────────────────┘

实施三步走

① 出参脱敏:@Sensitive 注解 + SensitiveJsonSerializer(本文第三、四章)
② 日志兜底:logback %replace 正则替换手机号/身份证/银行卡
③ 高敏字段:加密存储(AES + KMS 密钥管理),防拖库

关键数据

  • 改造成本:每个敏感字段 1 行注解,原 3 天的手动改造 → 半天完成
  • 性能开销:订单列表 100 条 × 5 敏感字段,P99 增幅 < 0.5ms
  • 覆盖范围:Controller 出参、Feign 编码、消息序列化统一生效
  • 动态脱敏:role 属性按角色豁免,客服主管看明文,普通用户看打码

一句话

脱敏的本质不是"把数据打码",而是"每个场景只看到它该看的部分"。Jackson 序列化器方案赢在三点:注解声明式零入侵、只在输出边界转换不改原对象、所有 JSON 出参统一兜底——把安全关切从业务代码里剥离出来,才不会出现"新接口忘了脱敏"这种防不住的事故。

给团队的建议

阶段建议
零脱敏先上 @Sensitive + Jackson 方案,覆盖出参,成本最低
出参已脱敏加 logback replace 日志兜底,防业务侧遗漏
有导出场景导出服务补 TypeHandler,低信任下游只见脱敏值
高敏数据银行卡/密钥类字段加密存储 + KMS,防拖库
长期治理建立敏感字段清单 + CI 扫描(出参 DTO 未加注解则告警)

互动话题:你们项目的脱敏是在哪一层做的?有没有踩过"脱敏值被写回数据库"或者"缓存里的数据被脱敏污染"的坑?动态脱敏(按角色看明文)你们是怎么实现的?评论区聊聊。


参考资料


标题:数据脱敏实战:手机号/身份证/银行卡——三行代码搞定,不侵入业务逻辑
作者:jiangyi
地址:http://jiangyi.space/articles/2026/09/03/1788097377007.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消