大数跨境
0
0

Spring Boot 接口敏感字段脱敏的5大"王炸"方案

Spring Boot 接口敏感字段脱敏的5大"王炸"方案 Spring全家桶实战案例
2025-08-27
0
导读:Spring Boot 接口敏感字段脱敏的5大"王炸"方案
Spring Boot 3实战案例锦集PDF电子书已更新至130篇!

🎉🎉《Spring Boot实战案例合集》目前已更新161个案例,我们将持续不断的更新。文末有电子书目录。

💪💪永久更新承诺

我们郑重承诺,所有订阅合集的粉丝都将享受永久免费的后续更新服务

💌💌如何获取
订阅我们的合集点我订阅,并通过私信联系我们,我们将第一时间将电子书发送给您。

→ 现在就订阅合集

环境:SpringBoot3.4.2



1. 简介

在实际项目中,经常会遇到一些敏感字段信息,像用户的身份证号码、银行卡卡号、详细家庭住址、手机号码以及各类密码等。若这些敏感字段在接口传输或展示过程中未作脱敏处理,一旦遭遇数据泄露事件,将给用户带来严重损失,包括财产被盗刷、个人隐私曝光,甚至可能引发身份盗用等风险。

接口敏感字段脱敏,通过对关键信息进行部分隐藏、替换或加密等操作,在不影响数据正常使用和分析的前提下,最大程度降低敏感信息暴露风险。

本篇文章我们将采用5种技术方案来实现敏感字段的脱敏。

2.实战案例

准备环境

public class User {  private Long id ;  private String name ;  private Integer age ;  private String phone ;  private String idNo ;  // getters, setters, constructors}// Controller接口@RestController@RequestMapping("/users")public class UserController {  @GetMapping  public ResponseEntity<Userquery() {    return ResponseEntity.ok(new User(666L, "Pack_xg"33      "13399065789""320311198512185678")) ;  }}

我们在编写一个工具类,统一都由该工具类处理敏感字段

public class MaskUtils {  public static String maskString(String input) {    int length = input.length();    // 计算xxx、***、yyy的长度(总长度=xxx+***+yyy)    int xxxLength = length / 3;        // 前1/3    int yyyLength = length / 3;        // 后1/3    // 中间剩余部分用*    int starLength = length - xxxLength - yyyLength;    // 确保至少各保留1个字符(避免xxx或yyy为0)    xxxLength = Math.max(1, xxxLength);    yyyLength = Math.max(1, yyyLength);    starLength = Math.max(1, starLength);    // 重新调整(防止因取整导致总和超出原长度)    while (xxxLength + starLength + yyyLength > length) {      if (xxxLength > 1) xxxLength--;      else if (starLength > 1) starLength--;      else yyyLength--;    }    String xxx = input.substring(0, xxxLength);    String yyy = input.substring(length - yyyLength);    String stars = repeat("*", starLength);    return xxx + stars + yyy;  }
  private static String repeat(String s, int count) {    return new String(new char[count]).replace("\0", s);  }}

2.1 自定义Json序列化

public class CommonMaskSerializer extends JsonSerializer<String> {  @Override  public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {    if (value == null || value.length() < 6) {      gen.writeString("***");      return;    }    String masked = MaskUtils.maskString(value) ;    gen.writeString(masked);  }}

接下来,修改实体对象如下:

public class User {  @JsonSerialize(using = CommonMaskSerializer.class)  private String phone ;  @JsonSerialize(using = CommonMaskSerializer.class)  private String idNo ;}

输出结果

2.2 自定义Jackson模块

上一种方法是字段级别的,虽然精准,但若多个实体都需要相同的逻辑,则会显得冗余繁琐。为了避免将所有内容都与注解绑定,我们可以通过自定义模块方式注册自定义序列化器,这些序列化器可应用于整个类型。如下代码实现:

@Configurationpublic class JacksonConfig {  @Bean  Jackson2ObjectMapperBuilderCustomizer maskingCustomizer() {    return builder -> builder.modules(new MaskingModule());  }  public static final class MaskingModule extends SimpleModule {    // 这里我们固定了处理那些字段名,你也可以将其放到配置文件中进行动态管理    private static final Set<String> fields = Set.of("idNo""phone") ;    public MaskingModule() {      setSerializerModifier(new BeanSerializerModifier() {        @Override        public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,            List<BeanPropertyWriter> props) {          for (int i = 0; i < props.size(); i++) {            BeanPropertyWriter w = props.get(i);            if (w.getType().isTypeOrSubTypeOf(String.class) && fields.contains(w.getName())) {              props.set(i, new MaskingWriter(w));            }          }          return props;        }      });    }  }  static final class MaskingWriter extends BeanPropertyWriter {    private static final long serialVersionUID = 1L;    MaskingWriter(BeanPropertyWriter base) {      super(base);    }    @Override    public void serializeAsField(Object bean, JsonGenerator gen, com.fasterxml.jackson.databind.SerializerProvider prov)        throws Exception {      Object raw = get(bean);      if (raw == null) {        super.serializeAsField(bean, gen, prov);        return;      }      String masked = MaskUtils.maskString(raw.toString());      gen.writeStringField(getName(), masked);    }  }}

接下来,我们将实体类上的@JsonSerialize注解删除,一样能达到效果,并且此种方式不需要我们对实体类进行任何的修改,这能满足绝大多数的场景。

2.3 使用AOP技术

利用AOP技术,在Controller接口返回后,通过反射技术对返回值进行处理。

自定义注解,只有返回值的类型上使用了该注解才进行处理

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface Mask {
  /**需要处理的字段*/  String[] value() default {} ;}

定义切面

@Aspect@Componentpublic class MaskAspect {  private static final Map<Class<?>, List<PropMethod>> cache = new ConcurrentHashMap<>();  @AfterReturning(pointcut = "execution(* com.pack.sensitive.controller..*.*(..))", returning = "response")  public void applyMasking(Object response) throws Throwable {    Object target = response;    if (response instanceof ResponseEntity entity) {      target = entity.getBody();    }    Class<?> clazz = target.getClass();    Mask mask = clazz.getAnnotation(Mask.class);    if (mask == null) {      return;    }    List<String> fields = Arrays.asList(mask.value());    if (fields.isEmpty()) {      return ;    }    List<PropMethod> props = cache.get(clazz);    if (props == null) {      BeanInfo beanInfo = Introspector.getBeanInfo(clazz);      for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {        if (fields.contains(pd.getName())) {          props = cache.computeIfAbsent(clazz, key -> new ArrayList<>()) ;          props.add(new PropMethod(pd.getReadMethod(), pd.getWriteMethod())) ;        }      }    }    if (props != null) {      for (PropMethod pm : props) {        pm.setter.invoke(target, MaskUtils.maskString((String) pm.getter.invoke(target))) ;      }    }  }  static record PropMethod(Method getter, Method setter) {  }}

修改User实体对象如下

@Mask({"phone""idNo"})public class User {  private Long id ;  private String name ;  private Integer age ;  private String phone ;  private String idNo ;}

调用Controller接口

2.4 使用ResponseBodyAdvice

我们可以利用Spring MVC的核心组件 ResponseBodyAdvice,该组件能够在控制器方法返回的响应体被序列化输出前,对返回数据进行拦截和处理。此种方式不是AOP技术,这里就完全使用反射技术实现脱敏。

在该方案中,我们还是利用Mask注解来进行标记处理,整体的处理与AOP基本相同。

@RestControllerAdvicepublic class MaskBodyAdvice implements ResponseBodyAdvice<Object> {  private static final Map<Class<?>, List<PropMethod>> cache = new ConcurrentHashMap<>();
  @Override  public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {    return true ;  }  @Override  public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,      Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,      ServerHttpResponse response) {    if (body == null) {      return null;    }
    if (body instanceof ResponseEntity entity) {      body = entity.getBody() ;    }    if (body instanceof Collection<?> coll) {      for (Object o : coll) {        try {          maskField(o);        } catch (Throwable e) {}      }      return body;    }    try {      maskField(body);    } catch (Throwable e) {}    return body;  }  private void maskField(Object target) throws Throwable {    // 这里的处理逻辑与上面的AOP一样   }}

此种方案核心与AOP一样,只是这里不需要代理技术而已。

2.5 使用@JsonFilter

我们还可以配置 Jackson 属性过滤器,也就是在需要处理的类上添加 @JsonFilter 注解,这样我们就不需要为每个对象单独添加注解的字段级过滤。

首先,修改实体对象

@JsonFilter("maskFilter")public class User {  // ...}

这里的名称 maskFilter 随意。

接下来,自定义ObjectMapper

@Configurationpublic class MaskFilterConfig {  private static final Set<String> fields = Set.of("idNo""phone");  @Bean  Jackson2ObjectMapperBuilderCustomizer maskingFilterCustomizer() {    return builder -> {      SimpleFilterProvider filters = new SimpleFilterProvider().addFilter("maskFilter"new SimpleBeanPropertyFilter() {        @Override        public void serializeAsField(Object pojo, JsonGenerator gen, SerializerProvider prov, PropertyWriter writer)            throws Exception {          if (fields.contains(writer.getName())) {            Object val = (writer instanceof BeanPropertyWriter bpw) ? bpw.get(pojo) : null;            if (val == null) {              writer.serializeAsField(pojo, gen, prov);              return;            }            gen.writeStringField(writer.getName(), MaskUtils.maskString(val.toString()));            return;          }          writer.serializeAsField(pojo, gen, prov);        }      });      builder.filters(filters);    };  }}

通过上面的方法也能也能达到对敏感字段的处理。


以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏

推荐文章

Spring Boot 实时广播消息,仅需3行代码

太强了!用SQL实现Excel文件的CRUD操作

高级开发!Spring Boot 请求全链路10个神级扩展点

高级开发!自定义@RequestMapping深度整合熔断降级功能

高级版@ResponseBody,接口响应数据格式完全自定义

告别超卖!Spring Boot + 悲观锁:1行代码解决并发难题

Spring Boot 记录Controller接口请求日志7种方式,第六种性能极高

Controller 接口竟有这些 "神操作"?

惊呆了!Controller接口返回值支持17种逆天类型

Spring Boot 通过@JsonComponent注解完全控制JSON数据

惊呆了!Spring Boot 通过这7种策略实现注入

太神了!Spring Boot+Socket.IO 一个注解搞定实时通信

【声明】内容源于网络
0
0
Spring全家桶实战案例
Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
内容 832
粉丝 0
Spring全家桶实战案例 Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
总阅读38
粉丝0
内容832