Spring Boot数据脱敏实战:注解+Jackson序列化器实现接口与日志双层脱敏
发布时间:2026/9/14 4:13:37来源:尧图网络
先说一个真实的场景有一次我们在测试环境排查问题运维从生产库拉了一份脱敏不彻底的数据到测试库结果测试环境接口日志里用户手机号、身份证号明文躺在Elasticsearch里差点被安全团队通报。从那次之后我彻底明白了一件事——Spring Boot项目里数据脱敏不是“上线前加个过滤器”的锦上添花而是从接口设计第一天就该考虑的基础能力。这个标题看着简单真正落地的时候涉及序列化机制、注解处理器、AOP切面、日志链路每一个点都有坑。这篇文章就是围绕Spring Boot项目实现数据脱敏完整梳理我在实际项目里用过的方案和踩过的坑。核心思路是“接口返回脱敏 日志链路脱敏”双层方案尽量不侵入业务代码。适合正在做接口开发的后端工程师、项目里被安全测试搞得头疼的团队以及准备把脱敏能力沉淀成公共模块的架构师。无论你是刚接触Spring Boot还是已经写了好几年这套设计都有可以直接抄作业的部分。1. 数据泄露往往发生在“不起眼”的位置——先从场景说起1.1 线上事故复盘不是核心库泄露而是日志和接口返回那次问题的根因不是数据库被拖而是接口返回给前端的报文里带了明文手机号前端埋点上报又把这些字段打到了第三方统计平台。这事让我认识到一个关键规律脱敏需求不是“核心业务库”的事而是任何会离开服务边界的输出都要管控。包括Controller返回给前端的JSON响应体服务间Feign调用、HTTP调用时传递的请求体和响应体日志框架打印的入参、出参、SQL参数异步消息队列发送的MQ消息体导出Excel、生成PDF报表时的数据源。这里面最容易被忽视的是日志。大家通常以为接口返回脱敏了就完事了结果一翻日志MyBatis打印的SQL参数里把身份证号、银行卡号全暴露了。排查问题的时候顺手贴一段日志到群里等于把敏感信息群发了一遍。所以脱敏设计真正要覆盖的是“所有输出路径”而不是某一个Controller。1.2 哪些字段必须脱敏先定义清楚再动手不是所有字段都需要脱敏也不是所有字段都有统一的脱敏规则。我在项目里做过一个敏感字段盘点按泄露影响面分成了三个等级敏感等级字段类型示例脱敏要求高身份证号、银行卡号、支付账号110101199003071234只保留前6后4中间打码高手机号13812345678保留前3后4中间打码中姓名、家庭住址张三、北京市朝阳区xx路xx号姓名保留姓氏地址保留到城市中邮箱、IP地址zhangsanqq.com保留前缀首字母和域名低座机号、车牌号010-88888888保留区号后几位打码这里有一个判断原则凡是能直接定位到具体个人的字段都按“高”等级处理。姓名这种看起来不算太敏感但如果和身份证号、手机号同时出现组合起来就是完整的人格画像所以一样要脱敏。1.3 脱敏不是“把数据弄坏”多个使用场景之间的平衡这里要提一个很多人容易走进的误区脱敏应该发生在“读出来给外部用”的阶段而不是“写入数据库”的阶段。业务系统内部很多时候是需要完整数据的比如发送短信验证码、做风控校验、财务对账。如果入库前就脱敏那这些业务基本全废了。所以我的设计原则是数据库里存明文输出边界做脱敏。具体来说服务端内部方法调用、服务间可信调用使用完整数据对外接口返回、日志打印、前端展示使用脱敏数据内部运维查询、数据分析平台通过独立的审批通道获取明文。这样既满足了业务需要又把泄露面限制在可控范围。后面要实现的方案就是围绕“输出边界脱敏”这条主线展开的。2. 脱敏方案选型为什么最终落在“注解 序列化器”上2.1 常见做法对比过滤器、拦截器、AOP、序列化器各有什么问题实现接口返回脱敏通常有几种思路我把它们放在一起对比过方案实现方式优点缺点Filter过滤器在OncePerRequestFilter里包装响应体对JSON字符串做正则替换不侵入业务代码正则容易误伤JSON解析性能差无法按字段精准控制HandlerInterceptor拦截器在postHandle里修改ModelAndView和Spring MVC集成好只能处理ModelAndViewResponseBody直接序列化场景不生效AOP切面拦截Controller方法在返回前对结果对象做反射遍历脱敏灵活能拿到方法注解要对每个脱敏字段写反射工具嵌套对象处理麻烦Jackson序列化器自定义JsonSerializer 自定义注解精准到字段级性能好嵌套天然支持只能作用于Jackson序列化的场景Fastjson等不生效上述方案里AOP切面和Jackson序列化器是我重点考虑的。AOP方案看起来灵活实际做起来有个绕不开的麻烦拿到返回对象之后做反射遍历需要处理各种嵌套泛型、Map、List、Set代码逻辑很容易变得又臭又硬。举个例子一个分页对象PageResultUserVO你要先解析出UserVO的泛型类型再找到UserVO里的敏感字段这个过程很容易漏。2.2 借助Spring Boot默认的Jackson机制实现“字段级精准脱敏”Spring Boot的HTTP消息转换器默认就是用Jackson完成对象到JSON的序列化。这意味着只要在字段上标注自定义注解并在Jackson里注册一个序列化器所有走ResponseBody的接口都会自动生效。这是最贴近Spring Boot原生机制、代码量最少、最好维护的方案。自定义序列化器的好处在于它工作在Jackson已经解析出字段值的阶段不存在字符串替换误伤的问题嵌套对象天然支持因为Jackson会递归处理内部字段性能开销非常低只对带注解的字段多一次字符串处理配合Jackson的ContextualSerializer可以在序列化时读取字段上的注解元数据一种Serializer处理多种脱敏类型。最终我的选择就是“自定义注解 Jackson序列化器 ObjectMapper定制器”三件套后面第3章会给出完整代码。2.3 为什么没有选Fastjson或者非Spring生态的JSON库有一个现实问题如果一个项目里已经大量使用了Fastjson的JSONField换方案的工作量确实大。如果你用的是Fastjson实现方式类似但方向得换成Fastjson的ValueFilter或者自定义序列化器。我的判断标准是这样的新项目、可以控制技术栈的项目优先用Jackson 注解因为Spring Boot默认就是Jackson不需要额外依赖遗留的Fastjson项目可以在ObjectMapper层面做一次适配或者使用Fastjson的ValueFilter统一拦截如果项目里混用多个JSON库Jackson、Fastjson、Gson那就要考虑在AOP层统一处理虽然麻烦但能避免“漏网之鱼”。我方案里选Jackson还有一个重要原因日志脱敏时Jackson的ObjectMapper可以直接复用保证日志输出的字符串和接口返回的字符串格式完全一致。这个细节后面会再提到。3. 核心实现四个手工组件打通脱敏链路3.1 步骤1定义脱敏类型枚举把规则集中管理脱敏规则最好集中定义不要散落在各个实体类里。我用一个枚举来管理所有脱敏类型和对应的正则规则public enum SensitiveType { // 手机号保留前3后4中间打码 MOBILE(mobile, 手机号, (\\d{3})\\d{4}(\\d{4}), $1****$2), // 身份证号支持15位和18位保留前6后4 ID_CARD(idCard, 身份证号, (\\d{6})\\d*(\\d{4}), $1********$2), // 银行卡号保留前6后4 BANK_CARD(bankCard, 银行卡号, (\\d{6})\\d*(\\d{4}), $1****$2), // 姓名保留姓氏名字打码 NAME(name, 姓名, (.).*, $1*), // 邮箱保留前缀第一个字符和后面的内容 EMAIL(email, 邮箱, (.)(.*)(.*), $1****$3), // 地址保留前6个字符 ADDRESS(address, 地址, (.{6}).*, $1****), // 自定义什么都不处理由具体场景传入自定义正则 CUSTOM(custom, 自定义, null, null); private final String code; private final String desc; private final String regex; private final String replacement; SensitiveType(String code, String desc, String regex, String replacement) { this.code code; this.desc desc; this.regex regex; this.replacement replacement; } public String desensitize(String value) { if (value null || value.isEmpty() || regex null) { return value; } return value.replaceAll(regex, replacement); } // getter... }这里有几个细节注意一下身份证号正则里的\\d*不能写成\\d{8}因为15位身份证中间只有6位数字18位身份证中间有8位数字用\\d*可以兼容姓名脱敏用(.).*匹配会把两个字的“张三”变成“张*”三个字的“张小明”变成“张*”四个字的“欧阳娜娜”变成“欧*”。如果业务要求保留前2后1就得单独定义正则邮箱脱敏用(.)(.*)(.*)只保留第一个字符和后面的域名。注意.*默认是贪婪匹配不影响这里的结果因为邮箱里只有一个。3.2 步骤2自定义注解SensitiveField标记需要脱敏的字段枚举定义好了接下来要定义一个注解用来标记实体类中的敏感字段Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) JacksonAnnotationsInside JsonSerialize(using SensitiveFieldSerializer.class) public interface SensitiveField { // 脱敏类型默认手机号 SensitiveType value() default SensitiveType.MOBILE; // 是否启用多级脱敏按用户角色默认不启用后面扩展会讲 boolean dynamic() default false; }注意一个关键设计我在自定义注解上直接加上了JsonSerialize(using SensitiveFieldSerializer.class)。这样做的目的是让脱敏逻辑和注解绑定在一起实体类上只需要标注SensitiveField不需要额外重复写JsonSerialize。像这样public class UserVO { private Long id; SensitiveField(SensitiveType.NAME) private String name; SensitiveField(SensitiveType.MOBILE) private String mobile; SensitiveField(SensitiveType.ID_CARD) private String idCard; SensitiveField(SensitiveType.EMAIL) private String email; SensitiveField(SensitiveType.BANK_CARD) private String bankCard; // getter/setter... }这个注解就是整套方案的“开关”。业务人员看实体类的时候一眼就能看出哪些字段是敏感的代码自解释性很强。3.3 步骤3自定义Jackson序列化器处理字段级别序列化序列化器是整个方案的核心组件。它要实现JsonSerializerString和ContextualSerializer两个接口。ContextualSerializer的createContextual方法会在Jackson初始化时被调用我们在里面读取字段上的SensitiveField注解拿到脱敏类型然后把序列化逻辑绑定到这个字段上public class SensitiveFieldSerializer extends JsonSerializerString implements ContextualSerializer { private SensitiveType type SensitiveType.CUSTOM; Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value null) { gen.writeNull(); return; } gen.writeString(type.desensitize(value)); } Override public JsonSerializer? createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { if (property null) { return prov.findNullValueSerializer(property); } // 判断字段类型只对String类型的字段生效 if (property.getType().getRawClass() String.class) { SensitiveField annotation property.getAnnotation(SensitiveField.class); if (annotation null) { annotation property.getContextAnnotation(SensitiveField.class); } if (annotation ! null) { this.type annotation.value(); return this; } } return prov.findValueSerializer(property.getType(), property); } }这里有一个很容易踩的坑必须通过property.getAnnotation()拿到注解而不是通过反射重新获取。因为createContextual方法在序列化器创建时执行拿到的是当前正在处理的字段上下文。使用注解类型的value()必须赋值否则默认就是CUSTOM类型而CUSTOM的正则是null会导致脱敏不生效。我建议枚举里CUSTOM类型的desensitize方法返回原值避免出现NPE。3.4 步骤4注册到Spring Boot的ObjectMapper定制器序列化器写好了如果只是new ObjectMapper()Spring Boot并不知道它的存在。需要通过Jackson2ObjectMapperBuilderCustomizer把它注册到Spring容器管理的主ObjectMapper中Configuration public class SensitiveFieldAutoConfiguration { Bean public Jackson2ObjectMapperBuilderCustomizer sensitiveFieldCustomizer() { return builder - { // 注册自定义序列化器到默认上下文 builder.serializerByType(String.class, new SensitiveFieldSerializer()); // 或者通过注解模块注册两种方式二选一 SimpleModule module new SimpleModule(SensitiveFieldModule); module.addSerializer(String.class, new SensitiveFieldSerializer()); builder.modules(module); }; } }第一种方式serializerByType(String.class, ...)会把所有String类型的序列化都交给这个序列化器通过createContextual方法判断字段上是否有注解有注解才走脱敏没有就回退到默认。这种方式简单直接但要注意它会影响所有String序列化路径如果项目中还有其他基于String类型的自定义序列化器可能会冲突。第二种方式通过SimpleModule注册效果类似。实际项目中我建议用第一种因为createContextual里已经做了大量的类型判断和注解判断回调逻辑足够安全。注册完之后所有Controller返回的对象只要字段上有SensitiveField注解输出的JSON里就会自动脱敏。测试一下{ name: 张*, mobile: 138****5678, idCard: 110***********1234, email: z****qq.com, bankCard: 6222***********1234 }3.5 关于Feign调用和HTTP调用的处理默认情况下Feign的编解码器使用的是容器里的HttpMessageConverters也就是和Controller返回走同一套Jackson配置。所以只要注册了上面的ObjectMapper定制器Feign请求体/响应体里带敏感注解的字段也会自动脱敏。这一点很关键意味着服务间调用时不会把明文手机号传到下一个服务里。如果你使用的是RestTemplate或WebClient需要手动确保它们使用的ObjectMapper是同一个定制后的实例。我建议在项目里定义一个全局的ObjectMapper Bean所有HTTP客户端都注入这个Bean而不是各自new一个。4. 独立于返回值的日志脱敏一条切面织入所有Mapper4.1 接口返回脱敏了日志却又漏了接口返回脱敏上线之后我原本以为这事就完了。结果安全测试同事丢了一份报告过来系统日志里打印了Service层入参和出参的完整对象手机号是明文的。这才意识到接口返回只是脱敏的一个出口日志打印是另一个更大的泄露出口。当时项目里日志打印普遍是这么写的log.info(查询用户信息请求参数{}, JSON.toJSONString(userQueryDTO)); log.info(查询用户信息返回结果{}, JSON.toJSONString(userVO));日志对象里包含完整的userVO结构虽然Controller返回时脱敏了但日志代码直接打印了Java对象Jackson的序列化器不会介入JSON.toJSONString()这个过程。脱敏在这个路径上完全失效。4.2 用AOP统一拦截Service出口自动打印脱敏日志我的解决思路是不依赖业务代码主动打印脱敏日志而是用一个AOP切面统一拦截Service方法自动打印脱敏后的入参和出参。定义一个注解LogDesensitize标记需要打印脱敏日志的方法Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface LogDesensitize { // 是否打印入参 boolean printArgs() default true; // 是否打印出参 boolean printResult() default true; // 操作描述 String desc() default ; }然后定义一个切面Aspect Component public class DesensitizeLogAspect { private static final Logger log LoggerFactory.getLogger(DesensitizeLogAspect.class); // 注入定制后的ObjectMapper Resource private ObjectMapper objectMapper; Around(annotation(logDesensitize)) public Object around(ProceedingJoinPoint joinPoint, LogDesensitize logDesensitize) throws Throwable { String methodName joinPoint.getSignature().toShortString(); if (logDesensitize.printArgs()) { Object[] args joinPoint.getArgs(); // 注意可能包含HttpServletRequest等无法序列化的类型需要过滤 String argsJson objectMapper.writeValueAsString(filterArgs(args)); log.info([{}]入参: {}, methodName, argsJson); } Object result null; try { result joinPoint.proceed(); return result; } finally { if (logDesensitize.printResult()) { String resultJson objectMapper.writeValueAsString(result); log.info([{}]出参: {}, methodName, resultJson); } } } private Object[] filterArgs(Object[] args) { return Arrays.stream(args) .filter(arg - !(arg instanceof HttpServletRequest) !(arg instanceof HttpServletResponse) !(arg instanceof BindingResult)) .toArray(); } }这个切面有个核心技巧它重用了定制后的ObjectMapper。因为我们的Jackson序列化器已经注册到这个ObjectMapper里所以在打印日志时实体类上标注了SensitiveField的字段会自动脱敏。这就保证了日志输出和接口返回的脱敏效果一致。使用的时候只需要在业务方法上标注Service public class UserServiceImpl implements UserService { LogDesensitize(desc 查询用户信息) Override public UserVO getUserById(Long id) { // 业务逻辑... } }日志输出[查询用户信息]入参: {id:1001} [查询用户信息]出参: {name:张*,mobile:138****5678,idCard:110***********1234}这样即使后来有人往实体类里加了新的敏感字段但忘了加脱敏注解只要日志切面覆盖到这个方法敏感信息不会因为日志输出而泄露。不过要补充一句如果实体类字段没加SensitiveField注解Jackson不会脱敏所以这个方案依赖前期的字段标注规范。4.3 日志链路里的SQL参数脱敏MyBatis日志如何收敛除了业务日志SQL日志也是一个风险点。MyBatis的MyBatis-Plus或者原生MyBatis打印SQL参数时会把PreparedStatement的参数原样打印出来。比如where mobile 13812345678这种直接暴露了明文手机号。解决思路有两个方向调整日志级别不打印SQL参数最简单粗暴但排查问题会不方便实现自定义Interceptor拦截MyBatis的ParameterHandler在日志输出前对参数脱敏。我项目中采用的是第二种方向拦截ParameterHandler.setParameters()在参数真正被设置到PreparedStatement之前不对参数做修改但会对需要打印的日志内容做临时替换。这里摘录关键思路Intercepts({ Signature(type ParameterHandler.class, method setParameters, args {PreparedStatement.class}) }) public class SensitiveParameterInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { // 这里可以在SQL日志输出前通过反射读取ParameterHandler中的参数对敏感类型做临时打码 return invocation.proceed(); } }说实话这个拦截器实现起来比预想中要麻烦因为MyBatis不同版本对ParameterHandler内部结构的实现有差异强依赖反射的方式在版本升级时很容易碎。所以我的建议是如果项目框架允许优先考虑把SQL日志级别调整到不打印参数值如果确实需要打印再考虑拦截器方案。4.4 日志脱敏后的可排查性不能脱敏到连问题都定位不了日志脱敏有一个平衡问题脱敏做过头了线上排查问题会非常痛苦。比如手机号全变成****那根据日志定位某个用户的问题就难了。我的处理技巧是脱敏日志里保留一个可关联的“业务跟踪ID”比如用户ID、订单号。这样既保护了敏感信息又保留了问题定位的线索。日志链路设计应该是请求ID: 8f0a2b3c-1234 用户ID: 1001 手机号: 138****5678通过用户ID关联查询审计系统拿到完整数据而不是直接在日志里打印明文。这个思路在分布式链路追踪里同样适用TraceId、SpanId天然就是关联凭证。5. 实测与踩坑序列化时机、NPE与嵌套对象5.1 坑1写了序列化器但没生效——ObjectMapper实例不一致这是最容易踩的坑。Spring Boot里ObjectMapper的实例来源有很多WebMvcConfigurer里自定义的、Bean手动创建的、Jackson2ObjectMapperBuilder构建的。如果你的项目里有人手动new ObjectMapper()并注册成了Bean那么我们的Jackson2ObjectMapperBuilderCustomizer可能根本不会作用到那个实例上。排查方法在序列化器createContextual和serialize方法里打日志看看到底有没有被调用。如果完全没有调用八成就是ObjectMapper实例不是同一个。解决方法是统一ObjectMapper实例Configuration public class ObjectMapperConfig { Bean Primary public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { return builder.build(); } }把自定义ObjectMapper定义为Primary所有注入ObjectMapper的地方都会拿到同一个实例。Spring Boot自动配置里的ObjectMapper也会被覆盖。5.2 坑2为空字段触发了脱敏逻辑导致NPE如果一个敏感字段值为null序列化器里如果直接调用type.desensitize(value)可能在正则匹配时触发空指针。很多人在字段为null的时候想的是“null不脱敏直接输出”但Jackson的序列化器必须显式处理null情况Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value null) { // 注意这里不是writeNull(): 会绕过序列化器走默认null处理 gen.writeNull(); return; } // 空字符串也直接返回避免正则处理空串 if (value.isEmpty()) { gen.writeString(value); return; } gen.writeString(type.desensitize(value)); }这个坑之所以隐蔽是因为在接口返回正常数据时根本不会触发只有当数据库里的敏感字段本身就是NULL时才会偶发出现。压测、联调阶段这类问题很难被发现等到线上数据质量参差不齐时才会暴露。5.3 坑3嵌套对象的脱敏失效——List、Map、泛型对象Jackson的序列化器支持嵌套调用但要求嵌套对象里的字段也标注了SensitiveField注解。例如一个分页结果public class PageResultT { private Long total; private ListT records; // getter/setter... }调用接口返回PageResultUserVO时Jackson会递归解析records里的UserVO对象UserVO里的sensitive注解字段会自动脱敏。这里不需要额外写代码。但有一个特殊情况Map类型。如果字段类型是MapString, ObjectJackson不会知道Map里的value是什么业务类型自然也就无法自动脱敏。例如// 这种场景脱敏不会生效 private MapString, Object extraInfo;这种场景下的处理方案是在Service层组装Map时手动调用脱敏工具类或者尽量使用强类型DTO避免用Map传输业务数据。5.4 坑4字段类型不匹配导致序列化器不生效SensitiveFieldSerializer实现的是JsonSerializerString并通过property.getType().getRawClass() String.class做了类型判断。如果字段类型是StringBuilder或者char[]这个序列化器根本不会绑定。所以脱敏字段必须是String类型或者自己扩展其他类型的序列化器。项目里有人遇到过把一个Long类型的字段加上SensitiveField想让Jackson处理比如把用户ID当成敏感字段结果发现注解完全没有生效。原因就在这里类型不匹配序列化器直接被跳过了。5.5 性能影响实测脱敏操作的性能损耗主要是replaceAll正则在每次序列化时执行一次。我做了个简单压测一万个包含手机号、姓名、身份证号、邮箱4个敏感字段的用户对象序列化对比场景耗时说明未启用脱敏序列化器约420ms直接走默认String序列化启用脱敏序列化器约450ms每个字段多了一次正则替换启用脱敏 日志AOP约480ms日志序列化了一次对象性能开销在7%左右对于绝大多数Web应用来说完全可接受。实际项目中单次接口查询可能只有几十个对象这个损耗基本可以忽略。如果字段数量巨大可以考虑把正则Pattern预编译代替String.replaceAll内部的每次编译。所以我建议在项目里定义一个静态Pattern缓存public class SensitiveDataUtil { private static final MapSensitiveType, Pattern PATTERN_CACHE new ConcurrentHashMap(); public static String desensitize(String value, SensitiveType type) { if (value null || value.isEmpty()) { return value; } Pattern pattern PATTERN_CACHE.computeIfAbsent(type, t - Pattern.compile(t.getRegex())); return pattern.matcher(value).replaceAll(type.getReplacement()); } }这样正则只编译一次后续全部复用性能损耗能进一步降低。6. 玩法升级同一接口不同角色看到不同数据6.1 需求场景管理员和普通运营看到的手机号不一样脱敏做到这一步基本能满足绝大多数项目需求。但实际业务往往还有更细的要求同一个用户列表接口管理员能看到完整手机号普通运营人员只能看到脱敏手机号。这个需求如果靠“接口返回脱敏”这种静态规则实现会很难办因为序列化器本身无状态它不知道当前请求是谁。所以需要引入“动态脱敏”能力。6.2 基于自定义策略注解实现动态脱敏首先要给SensitiveField注解增加一个可选的策略标识public interface SensitiveField { SensitiveType value() default SensitiveType.MOBILE; // 脱敏策略bean名称从Spring容器中动态获取 String strategy() default ; }在序列化器里当strategy不为空时通过Spring容器获取对应的DesensitizeStrategy实现类由策略类决定当前用户应该看到明文还是脱敏文public class SensitiveFieldSerializer extends JsonSerializerString implements ContextualSerializer { private SensitiveType type; private String strategyName; Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value null) { gen.writeNull(); return; } String result value; if (StringUtils.hasText(strategyName)) { // 从Spring容器中获取策略Bean DesensitizeStrategy strategy SpringContextHolder.getBean(strategyName); result strategy.desensitize(value, type); } else { result type.desensitize(value); } gen.writeString(result); } Override public JsonSerializer? createContextual(SerializerProvider prov, BeanProperty property) { SensitiveField annotation property.getAnnotation(SensitiveField.class); if (annotation ! null) { this.type annotation.value(); this.strategyName annotation.strategy(); return this; } return prov.findNullValueSerializer(property); } }DesensitizeStrategy是一个函数式接口public interface DesensitizeStrategy { String desensitize(String value, SensitiveType type); }举个例子判断当前用户是否有权限看到明文Component(phoneAdminStrategy) public class PhoneAdminStrategy implements DesensitizeStrategy { Override public String desensitize(String value, SensitiveType type) { // 判断当前登录用户角色 if (isAdmin()) { return value; // 管理员看完整数据 } return type.desensitize(value); } private boolean isAdmin() { // 从SecurityContext或ThreadLocal获取当前用户角色 return false; } }实体类这样标注SensitiveField(value SensitiveType.MOBILE, strategy phoneAdminStrategy) private String mobile;这属于进阶玩法实现会稍微复杂一点但思路是通的。核心还是把“脱敏规则”和“用户上下文”解耦让脱敏逻辑具备感知当前请求的能力。6.3 项目落地时的规范建议最后根据我的项目经验给几条实操层面的建议敏感字段盘点要做成表格每一张表、每一个字段都过一遍确定脱敏类型这是所有工作的基础实体类分层之后统一管理注解VO、DTO这些出参对象必须标注Entity实体内部不标避免内部调用受影响把脱敏能力封装成公共模块打成starter给多个服务复用不要每个服务各自实现一套上线前用安全测试工具扫一遍接口重点看枚举、异常信息这些容易被忽略的地方定期检查日志平台用一些简单的正则规则扫一下日志中是否出现手机号、身份证号明文。数据脱敏这件事技术上不难难的是把它变成团队规范、落地到每个输出路径。Jackson序列化器这套方案就是尽量让开发人员少操心、框架多承担。从实际运行效果看稳定性和维护成本都让我比较满意。后续如果有精力我可能会再扩展一个针对MQ消息体的脱敏拦截器让整个数据出口的覆盖面更完整。
网站建设高端定制企业官网