Spring Boot数据脱敏实战:注解+Jackson序列化器优雅实现
2026/9/8 1:11:52 网站建设 项目流程

数据脱敏这事,我在实际项目里踩过不少坑。很多人以为给前端返回数据时把手机号打几个星号就完事了,但真正落到 Spring Boot 项目里,会发现要处理的问题远比想象中多:日志里明文泄露、接口返回脱敏不彻底、嵌套对象里的敏感字段漏网、脱敏规则写死在业务代码里难以维护……这篇文章我把自己的完整实现思路和踩坑记录整理出来,希望对你有点帮助。

1. 数据脱敏的场景与方案选型

1.1 哪些场景必须做数据脱敏

数据脱敏不是锦上添花,而是很多系统的硬性合规要求。我总结下来,日常项目里最常见的敏感数据场景有这么几类:

  • 个人隐私信息:手机号、身份证号、银行卡号、邮箱、家庭住址等,这些都是最常见需要打码的字段。
  • 业务敏感信息:订单号、流水号、部分业务编码,虽然不属于个人隐私,但暴露给前端或日志系统也有风险。
  • 内部系统交互数据:微服务之间调用的响应体、消息队列中的消息体,如果包含敏感字段,同样需要处理。
  • 日志输出:这个经常被忽略。接口返回做了脱敏,但日志里打出来的参数或响应值还是明文,等于白干。

触发脱敏的时机也分几种:接口返回值序列化时脱敏、日志输出时脱敏、数据库查询结果脱敏、文件导出脱敏。不同时机对应不同技术方案,先想清楚自己系统需要覆盖哪几个点,再选择方案。

1.2 三种主流实现方案对比

Spring Boot 项目里实现数据脱敏,主流方案大概有三条路,我分别说下优缺点和适用场景。

方案一:自定义注解 + Jackson 序列化器

在实体类字段上标注自定义注解,通过重写 Jackson 的序列化器,在对象转 JSON 输出的过程中自动完成脱敏。这是目前最推荐的做法,侵入性最低、最灵活,可以精确到字段级别控制。

字段加注解 -> Jackson 序列化时读取注解 -> 调用对应的脱敏逻辑 -> 输出脱敏后的 JSON

方案二:MyBatis 拦截器 / 数据库层脱敏

通过拦截 SQL 或对查询结果做统一处理。这样做的好处是数据从持久层出来就已经是脱敏的,接口层和日志层都不用重复处理。但缺点是影响面太大,一个拦截器会作用在所有查询上,很容易误伤不需要脱敏的字段或接口,而且如果多个系统共用一个数据库,反而导致别处拿不到明文数据。

方案三:业务代码手动调用脱敏工具类

在 VO 层或 Controller 层手动调用SensitiveUtil.maskPhone(mobile)之类的方法。这种方式最直接,但代码侵入性最强,每个需要脱敏的接口都要写一行调用逻辑,字段一旦多起来,代码会非常啰嗦,而且容易漏掉某个接口。

综合对比下来,我推荐方案一。它把脱敏逻辑收敛到序列化层,业务代码无感知,新增脱敏字段只需要加一个注解,维护成本最低。这也是我后面要展开细说的方案。

1.3 为什么最终选择了 Jackson 序列化器方案

选择这个方案,核心原因是它解决了我在以往项目里最头疼的几个问题。

第一,和 Spring Boot 深度集成。Spring MVC 默认使用 Jackson 做消息转换器,我们不需要引入额外的 JSON 库,也不需要对现有 Controller 代码做任何改动,只要注册好自定义序列化器,所有@ResponseBody接口立刻生效。

第二,字段级精确控制。通过注解加在字段上,哪些字段脱敏、用什么规则脱敏一目了然,相比全局拦截器那种无差别处理,可控性强得多。

第三,和业务逻辑解耦。脱敏是表现层的需求,不应该污染 Service 层的业务逻辑。用注解方案,Service 层返回的对象仍然是明文,只有输出到前端时才被脱敏,这符合分层架构的设计原则。

第四,方便嵌套对象和集合处理。只要让自定义序列化器实现ContextualSerializer,就能在序列化任意层级字段时正确读取到注解上的配置,嵌套 DTO、List 里的对象都能覆盖到。

当然,这个方案也有一些局限,比如脱敏是单向的,脱敏后数据无法还原;比如它只能处理 JSON 输出场景,日志脱敏还需要另外配合。这些我放到后面的常见问题部分再展开。

2. 核心细节解析:注解与脱敏规则设计

2.1 定义脱敏类型枚举

脱敏规则要统一管理,第一步是定义一组枚举类型。这一步看似简单,但枚举的粒度设计会直接影响后续的可维护性。我见过有人把脱敏类型做得特别细,比如MOBILE_PHONEMOBILE_PHONE_FULL,实际使用下来反而让人困惑。我推荐的粒度是:一个枚举对应一种有明确含义的脱敏规则。

public enum SensitiveType { /** * 用户名,保留第一位和最后一位 */ USERNAME, /** * 手机号,保留前 3 位和后 4 位 */ MOBILE_PHONE, /** * 身份证号,保留前 4 位和后 4 位 */ ID_CARD, /** * 银行卡号,保留前 6 位和后 4 位 */ BANK_CARD, /** * 邮箱,保留前缀前 3 位和后缀域名 */ EMAIL, /** * 地址,仅保留省市区 */ ADDRESS, /** * 自定义规则(配合注解上的其他属性使用) */ CUSTOM }

这里我额外定义了一个CUSTOM类型,用于那些不好归类的字段。比如某个字段需要保留前 2 位和最后 3 位,这种规则如果都塞进枚举里,枚举会被撑得很大,用一个支持自定义正则的 CUSTOM 类型会更灵活。当然,如果项目里没有这种诉求,去掉它也行,不必过度设计。

2.2 自定义注解 @Sensitive

有了枚举之后,我们定义一个字段级别的注解,让实体类通过加注解的方式声明需要脱敏的字段。

@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @JsonSerialize(using = SensitiveInfoSerializer.class) public @interface Sensitive { /** * 脱敏类型 */ SensitiveType value() default SensitiveType.CUSTOM; /** * 自定义脱敏正则,仅 CUSTOM 类型生效 */ String pattern() default ""; /** * 自定义脱敏替换字符,仅 CUSTOM 类型生效 */ String replacement() default "****"; }

这里有两个关键点要说明。

第一,@JacksonAnnotationsInside是让注解组合生效的关键。没有这个注解,Jackson 不会把@Sensitive当作一个“复合注解”来解析,@JsonSerialize的配置就无法通过@Sensitive触发。我刚开始写这个功能时漏了这一步,结果注解一直不生效,排查了很久才发现是这个细节。

第二,@JsonSerialize(using = SensitiveInfoSerializer.class)直接把序列化器和注解绑定。这样在序列化阶段,Jackson 看到带@Sensitive的字段就会走SensitiveInfoSerializer这个序列化器。

2.3 脱敏工具类实现要点

脱敏的核心逻辑是字符串处理,我单独抽了一个工具类。每个脱敏方法都写得尽量清晰,并且在正则匹配上做了性能优化,避免每次调用都重新编译Pattern

public class SensitiveInfoUtils { private static final Pattern MOBILE_PHONE_PATTERN = Pattern.compile("(\\d{3})\\d{4}(\\d{4})"); private static final Pattern ID_CARD_PATTERN = Pattern.compile("(\\d{4})\\d{10}(\\w{4})"); private static final Pattern BANK_CARD_PATTERN = Pattern.compile("(\\d{6})\\d+(\\d{4})"); private static final Pattern EMAIL_PATTERN = Pattern.compile("(\\w{3})\\w+(@\\w+\\.[a-z]+)"); private SensitiveInfoUtils() { } public static String desensitize(String value, SensitiveType type) { if (StringUtils.isBlank(value)) { return value; } switch (type) { case USERNAME: return maskUsername(value); case MOBILE_PHONE: return MOBILE_PHONE_PATTERN.matcher(value).replaceAll("$1****$2"); case ID_CARD: return ID_CARD_PATTERN.matcher(value).replaceAll("$1********$2"); case BANK_CARD: return BANK_CARD_PATTERN.matcher(value).replaceAll("$1****$2"); case EMAIL: return EMAIL_PATTERN.matcher(value).replaceAll("$1***$2"); case ADDRESS: return maskAddress(value); default: return value; } } }

之所以把Pattern定义为静态常量,是因为正则表达式的compile是一个相对耗时的操作,如果每个字段每次序列化都重新编译一次,在高频调用场景下会产生不必要的开销。用静态常量预编译,代价几乎可以忽略。

maskUsernamemaskAddress这两个方法我单独说一下,因为正则写起来会比较绕:

private static String maskUsername(String username) { int length = username.length(); if (length <= 1) { return "*"; } if (length == 2) { return username.charAt(0) + "*"; } return username.substring(0, 1) + "*".repeat(length - 2) + username.substring(length - 1); } private static String maskAddress(String address) { if (address.length() <= 6) { return "*".repeat(address.length()); } return address.substring(0, 3) + "*".repeat(address.length() - 6) + address.substring(address.length() - 3); }

用户名和地址的脱敏没有统一的定长规则,不同系统要求可能不一样,放在这里作为一个可改的兜底实现。比如有的系统要求用户名保留第一个字和最后一个字,中间用星号填充,maskUsername这种动态长度的写法就能直接满足。

2.4 设计细节:为什么脱敏逻辑放在序列化层而不是业务层

这个问题我在 1.3 里大致提过,这里再往深了说一下。脱敏的本质是数据在展示或传输前做一次不可逆的转换。从职责划分的角度看,Service 层应该关心业务规则,DAO 层应该关心数据存取,而“如何把数据安全地输出给外部”这件事,本质上是接口层的职责。

把脱敏放在 Jackson 序列化层,相当于在系统边界做了一道统一的数据出口检查。业务逻辑模块完全不需要知道自己返回的数据会被怎么处理,专注自己的职责即可。反过来,如果把脱敏写进业务代码,一旦脱敏规则调整,比如从“保留前 3 位”改成“保留前 4 位”,就要把所有调用点全找出来改一遍,这是非常典型的维护噩梦。

另外一点,序列化层脱敏天然对所有通过@ResponseBody输出的对象生效。包括统一响应体Result<T>里嵌套的对象、List 集合里的元素,只要字段加了注解,就一定会被处理,不需要在每一层 Controller 里重复编写脱敏调用。

3. 实操过程与核心环节实现

3.1 环境准备与项目结构

我用的是 Spring Boot 2.7 版本,Java 8,不需要额外引入其他 JSON 库。整个实现涉及的类不多,项目结构大概是这样的:

com.example.demo ├── DemoApplication.java ├── common │ ├── annotation │ │ └── Sensitive.java │ ├── enums │ │ └── SensitiveType.java │ ├── serializer │ │ └── SensitiveInfoSerializer.java │ └── utils │ └── SensitiveInfoUtils.java ├── controller │ └── UserController.java ├── entity │ └── UserVO.java └── vo └── Result.java

需要说明的是,这只是一个最小可运行的结构。实际项目里,注解和序列化器通常放在公共模块或 starter 中,这样多个服务可以复用同一套脱敏能力,避免代码重复拷贝。如果你所在的公司有自研的公共组件库,建议直接把脱敏功能作为一个 starter 发布,服务方只需要引入依赖即可。

3.2 核心序列化器的完整实现

这是整个方案的心脏。SensitiveInfoSerializer继承了JsonSerializer<String>,并实现了ContextualSerializer接口。后者是关键,它允许序列化器在创建时读取到字段上的注解信息,从而动态决定当前字段使用哪种脱敏规则。

public class SensitiveInfoSerializer extends JsonSerializer<String> implements ContextualSerializer { private final SensitiveType type; public SensitiveInfoSerializer() { this.type = SensitiveType.CUSTOM; } public SensitiveInfoSerializer(SensitiveType type) { this.type = type; } @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeString(SensitiveInfoUtils.desensitize(value, type)); } @Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { if (property != null) { Sensitive sensitive = property.getAnnotation(Sensitive.class); if (sensitive != null) { return new SensitiveInfoSerializer(sensitive.value()); } } return this; } }

createContextual的调用时机是:Jackson 在第一次序列化某个类时,会为类中的每个字段寻找合适的序列化器。如果序列化器实现了ContextualSerializer,Jackson 就会把该字段的BeanProperty传进来,让我们有机会读取字段上的注解信息并创建新的、带脱敏类型配置的序列化器实例。

这里有个细节容易踩坑:serialize方法里拿到的value未脱敏的原始字符串,我们在这里执行脱敏逻辑,然后把脱敏后的结果写回 JSON。所以 Service 层拿到的、数据库里存的仍然是明文,只有最终输出的 JSON 是脱敏后的,这一点在排查问题时很有用。

3.3 注册序列化器到 Jackson

序列化器写好了,接下来的问题是怎么让它生效。有两种方式,我推荐第一种。

方式一:启用注解驱动 + 注解绑定(推荐)

我们在@Sensitive注解上已经加了@JsonSerialize(using = SensitiveInfoSerializer.class),所以只要保证 Spring 容器里的ObjectMapper开启了注解扫描即可。默认情况下 Spring Boot 的ObjectMapper是开启的,但为了保险起见,可以在配置类里显式设置:

@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer sensitiveDataCustomizer() { return builder -> builder .serializerByType(String.class, new SensitiveInfoSerializer()) .featuresToEnable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } }

等一下,如果你直接把serializerByType(String.class, new SensitiveInfoSerializer())配成对String类型生效,那所有的字符串都会走这个序列化器,等于把整个项目所有字符串字段全部进入SensitiveInfoSerializer.serialize,然后因为默认 type 是CUSTOM,最终原样返回,逻辑上没问题,但白白多了一层调用开销,而且容易干扰其他序列化器。

所以更稳妥的做法是:用注解驱动,不全局注册 String 类型处理器@JsonSerialize(using = SensitiveInfoSerializer.class)已经足够让 Jackson 在序列化带注解的字段时自动找到对应序列化器了,不需要额外往ObjectMapper里注册。

@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder -> builder .featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS) .featuresToEnable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } }

如果你的项目里没有这种全局配置类,也完全不影响注解生效。Spring Boot 默认会扫描@JsonSerialize注解。

3.4 实体类中使用注解

工具类和序列化器都就位后,使用起来非常简单。在实体类或 VO 类的敏感字段上加上@Sensitive注解并指定脱敏类型即可。

public class UserVO { private Long id; @Sensitive(SensitiveType.USERNAME) private String username; @Sensitive(SensitiveType.MOBILE_PHONE) private String mobile; @Sensitive(SensitiveType.ID_CARD) private String idCard; @Sensitive(SensitiveType.EMAIL) private String email; @Sensitive(SensitiveType.ADDRESS) private String address; private Integer age; // getter / setter 省略 }

Controller 层不需要做任何特殊处理:

@RestController @RequestMapping("/user") public class UserController { @GetMapping("/{id}") public Result<UserVO> getUser(@PathVariable Long id) { UserVO user = userService.getById(id); return Result.success(user); } }

请求/user/1接口时,返回的 JSON 大致长这样:

{ "code": 200, "message": "success", "data": { "id": 1, "username": "张*三", "mobile": "138****1234", "idCard": "1101**********1234", "email": "abc***@example.com", "address": "北京市******海淀区", "age": 25 } }

注意,idage没有加注解,所以不受影响。脱敏逻辑对普通字段完全透明,一个注解搞定一个敏感字段,代码非常干净。

3.5 嵌套对象和集合的处理机制

实体类里嵌套了其他对象,或者返回的是List<UserVO>,这种场景下脱敏还能生效吗?答案是能。因为我们的序列化器是实现ContextualSerializer的,Jackson 在处理嵌套对象和集合元素时,会递归地为每个对象的每个字段创建序列化器。所以只要嵌套对象内部的字段也加了@Sensitive,就能正常脱敏。

举个例子,订单 DTO 里嵌套了用户地址信息:

public class OrderVO { private String orderNo; private UserAddressVO address; // getter / setter 省略 } public class UserAddressVO { private String province; private String city; @Sensitive(SensitiveType.ADDRESS) private String detailAddress; // getter / setter 省略 }

序列化OrderVO时,Jackson 会递归处理address字段,最终detailAddress也会被脱敏。这种能力对于订单列表、用户详情这类多层嵌套的接口非常实用,不需要额外编码。

4. 常见问题与排查技巧实录

4.1 脱敏不生效的排错清单

我在自己项目和帮同事排查时,遇到最多的问题就是“注解加了,但返回结果还是明文”。原因排查可以从这几个方向依次检查:

1. 确认字段有 getter/setter(Jackson 依赖 getter 取值的) 2. 确认实体的 getter 方法上没加 @JsonIgnore 之类的注解 3. 确认项目里没有自定义的 ObjectMapper 覆盖了注解扫描配置 4. 确认注解的 @Retention 是 RUNTIME(如果是 CLASS 就无效) 5. 确认 @JacksonAnnotationsInside 没有被误删 6. 确认序列化的是该实体,而不是被转成了 Map 或其他结构

第 3 条值得多说一句。有些项目为了统一处理日期格式或 Long 转 String,会自定义ObjectMapper,甚至在配置里手动注册了各种模块。这种情况下如果新注册的SimpleModule覆盖了 String 类型的序列化器,就可能导致@Sensitive失效。解决办法是把自己定义的序列化器也注册进去,或者改用Jackson2ObjectMapperBuilderCustomizer这种方式追加配置,而不是直接 new 一个ObjectMapper覆盖 Spring 的默认配置。

还有第 6 条,如果 Controller 返回的是Map<String, Object>,Jackson 对 Map 的值做序列化时不会读取字段上的注解,脱敏当然不生效。数据量不大的时候依然能正常脱敏,但如果实体被转换成了MapJSONObject,就会发现注解失效。遇到这种情况,优先规范接口返回类型,尽量使用 DTO/VO 而不是Map裸返回。

4.2 字段为 null 或空串的边界情况

SensitiveInfoUtils.desensitize里我开头就判断了StringUtils.isBlank(value),返回空或 空字符串时原样返回,不做脱敏。这个判断是必须的,否则""传入正则匹配器会直接抛异常。

字段为null时,Jackson 默认不会调用serialize方法,而是走nullValueSerializer。这意味着@Sensitive注解对 null 值的字段是“无感”的,不会报错,也不会把 null 变成空字符串或星号。如果你的业务希望 null 也显示为某种占位符,需要在序列化器里额外处理null序列化逻辑,或者在实体字段上直接初始化默认值。

4.3 大对象批量序列化的性能优化

脱敏本质上是在序列化过程中做了额外的字符串处理和正则匹配,性能损耗主要在正则表达式上。如果一次接口返回几千条用户记录,每条记录里有 3~4 个敏感字段,正则匹配的压力还是不小的。

优化手段主要有三个,我在 2.3 里提到过预编译 Pattern,这是最基础也最有效的一步。第二是减少不必要的脱敏调用,比如@Sensitive加在字段上后,如果该字段本身已经是脱敏态的数据(比如某些老系统从库里读出来就是脱敏的),可以再加一个注解属性alreadyMasked = true跳过二次处理。第三是缓存序列化器实例createContextual理论上只会在字段首次序列化时调用一次,后续会复用,所以本身性能开销不大,但如果你的实体类特别多,留意不要在createContextual里做太重的初始化操作。

从我实测的数据来看,用预编译 Pattern 的方式,单次脱敏一个字段的开销在微秒级别,一个接口脱敏上千条记录总耗时也就增加几十毫秒,对绝大多数业务系统来说完全可接受。如果真到了百万级列表导出的场景,那就不是优化正则的问题了,得考虑在导出文件时用流式处理或者用批量线程池来做。

4.4 脱敏接口与日志脱敏的配合

接口返回脱敏和日志脱敏是两回事。注解方案只解决 JSON 输出时的脱敏,如果项目在 Controller 层打印了请求参数日志,或者在全局异常处理器里打印了响应体,日志里仍然可能记录明文敏感数据。

解决日志脱敏,通常是用 logback 的MessageConverterPatternLayout加正则过滤,比如在logback.xml里配置对手机号、身份证号做全局替换。也有团队用logstash-logback-encoder结合自定义字段脱敏。这块和接口脱敏是互补关系,没有哪个方案能两处同时覆盖,我是强制要求项目里所有日志打印规范统一,禁止在日志中输出业务对象的toString()全量内容,尤其控制层和持久层查询参数日志。

4.5 脱敏的不可逆性预警

这是很多初次接触脱敏的人容易忽略的点。脱敏本质上是不可逆的,脱敏后的数据无法还原成明文。所以一旦数据输出后又被前端保存、又被别人转发,那这个状态就是终态了。我们在接口文档里都会明确标注“脱敏字段不支持回显明文”。

另外,如果存在“展示脱敏、点击查看详情时需要明文”这样的业务场景,不能依赖脱敏后的数据去做二次请求。正确做法是:列表接口脱敏,详情接口根据权限判断是否需要明文。权限通过后返回明文,否则也走脱敏。这里要特别提醒,不要把脱敏后的值作为参数回传到后端来查数据库,因为脱敏值本身已经丢失了精度,查询会失败或得到错误结果。

4.6 脱敏规则的可配置化扩展思路

注解方案有个潜在短板:脱敏规则是硬编码在枚举和工具类里的,一旦规则变化就要改代码重新发版。如果你的项目对规则灵活性要求高,可以考虑把脱敏规则外部化,做成配置中心动态下发。

我自己的做法是引入了一个脱敏规则表,结构大概是:脱敏类型、正则表达式模式、替换格式、是否启用。核心序列化器不直接写死规则,而是调用一个SensitiveRuleProvider接口,从内存缓存中读取规则。配置变更时刷新缓存,不需要重启服务。

public interface SensitiveRuleProvider { SensitiveRule getRule(SensitiveType type); }

不过这套方案需要额外的配置管理成本,如果规则基本稳定,我建议还是用最简单直接的硬编码方案,避免过度设计。我之前就见过一个团队,把脱敏规则搞成数据库配置,结果规则被人误改后,线上脱敏直接失效,反而引入了新的风险。规则稳定时,代码里的常量才是最可靠的。

5. 实际部署中的补充建议

5.1 统一脱敏组件的沉淀

如果你所在的项目组有多个微服务,每个服务都复制一份脱敏代码是很大的维护负担。我建议把脱敏功能封装成一个公共 starter,包含注解、枚举、序列化器、工具类。各服务只需在pom.xml中引入依赖,实体类里加注解即可。

封装 starter 时有几个注意点:注解包名和序列化器包名要稳定,因为业务代码中引用了这些类;spring.factories或自动配置类要正确声明,让引入方无需额外配置就能生效;版本号管理要规范,避免升级时产生兼容性问题。

5.2 与统一响应体的适配经验

很多项目有统一的响应体Result<T>,数据放在data字段里。只要data中的实体字段加了@Sensitive注解,脱敏依旧生效,因为 Jackson 序列化的是整个Result<T>对象,会递归遍历到内部的data。这里没有特殊的适配工作,唯一要注意的是,不要把data字段声明成Object类型后塞入一个已经被转成 JSONString 的字符串,那样序列化出来的是字符串内容而非结构化对象,注解自然无法生效。

5.3 测试用例怎么写才能覆盖全面

很多人写完脱敏功能后忘了写测试。脱敏逻辑虽然简单,但边界条件不少,建议至少覆盖这几类用例:

  • 正常长度字段(11 位手机号、18 位身份证号)
  • 空字符串和 null
  • 长度极短的字段(比如用户名只有 1 个字)
  • 包含特殊字符或格式不规范的手机号(比如加了空格或横线)
  • 嵌套对象和 List 集合中的脱敏
  • 脱敏规则变更后的回归验证

MockMvc调用接口验证 JSON 输出是最直接的方式。如果是工具类的单元测试,直接断言脱敏后的字符串即可。这里多说一句,脱敏本来是为了保护数据安全,测试时也别忘了用假数据,别把真实用户信息写死在测试用例里,我之前见过有同事直接把生产环境的手机号写进单元测试断言,这种习惯很危险。

我个人做这类功能最大的体会是:脱敏看着是个小功能,但它涉及接口协议、数据安全、性能、可维护性多个维度,真正做好还是需要花点心思的。如果项目里还没有一套统一的脱敏机制,从自定义注解加序列化器这套方案开始改造,是成本最低、见效最快的路径。你完全可以拿这篇文章里的代码直接改改,落到自己的项目里去试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询