Backend Development 7 min read

Secure Sensitive Data in SpringBoot: AOP vs ResponseBodyAdvice Encryption

This article explains how to protect sensitive fields in SpringBoot 2.7 by using a custom annotation with AOP or a ResponseBodyAdvice implementation, comparing their advantages, showing complete code examples, and highlighting performance considerations for symmetric AES encryption.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Secure Sensitive Data in SpringBoot: AOP vs ResponseBodyAdvice Encryption

Environment: SpringBoot 2.7.18

1. Introduction

Encrypting sensitive fields during network transmission is essential for data security. Traditional encryption requires explicit calls in business code, increasing complexity and risk of omission. This article introduces two approaches to simplify this process in SpringBoot:

Custom annotation with AOP – No need to call encryption functions directly in business code, reducing code clutter and security risks.

Custom ResponseBodyAdvice – Handles data at the controller response level, avoiding proxy creation and offering slightly better performance.

Encryption impacts performance; choose algorithms wisely—symmetric encryption (AES) is generally preferred.

2. Practical Examples

2.1 AOP Implementation

Custom Annotation

<code>@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Sensitive {
  /** fields that need processing */
  String[] value() default {} ;
}</code>

Encryption Component

<code>@Component
public class SecretProcessor {
  private static final String ALG = "AES" ;
  @Value("${pack.crypto.secretKey}")
  private String secretKey ;
  private SecretKeySpec keySpec = null ;
  private Cipher cipher = null ;

  @PostConstruct
  public void init() {
    keySpec = new SecretKeySpec(this.secretKey.getBytes(), ALG) ;
    try {
      cipher = Cipher.getInstance("AES") ;
      cipher.init(Cipher.ENCRYPT_MODE, keySpec) ;
    } catch (Exception e) {
      // handle exception
    }
  }

  public String encrypt(String value) {
    try {
      return Base64.getEncoder().encodeToString(cipher.doFinal(value.getBytes())) ;
    } catch (Exception e) {
      return value;
    }
  }
}
</code>

Field Processing Component

<code>@Component
public class SensitivePropsProcessor {
  @Resource
  private SecretProcessor secretProcessor ;

  public void processor(Object data, List<String> props) {
    if (data == null || props == null || props.isEmpty()) {
      return ;
    }
    if (data.getClass().isPrimitive() || data instanceof String) {
      return ;
    }
    switch (data) {
      case List<?> list -> {
        list.forEach(item -> processorProp(item, props));
      }
      default -> processorProp(data, props);
    }
  }

  private void processorProp(Object data, List<String> props) {
    if (data.getClass().getPackageName().startsWith("com.pack.domain")) {
      for (String prop : props) {
        PropertyDescriptor pd = new PropertyDescriptor(prop, data.getClass()) ;
        Object value = pd.getReadMethod().invoke(data) ;
        pd.getWriteMethod().invoke(data, secretProcessor.encrypt(value.toString())) ;
      }
    }
  }
}
</code>

Aspect Definition

<code>@Component
@Aspect
public class SensitiveAspect {
  @Resource
  private SensitivePropsProcessor sensitivePropsProcessor ;

  @Pointcut("@annotation(sensitive)")
  private void pc(Sensitive sensitive) {}

  @AfterReturning(value = "pc(sensitive)", returning = "retValue")
  public void processRetValue(JoinPoint jp, Object retValue, Sensitive sensitive) {
    String[] props = sensitive.value() ;
    if (props.length > 0) {
      this.sensitivePropsProcessor.processor(retValue, List.of(props)) ;
    }
  }
}
</code>

Test Controller

<code>@Sensitive({"password", "idNo"})
@GetMapping("/{id}")
public User getUser(@PathVariable("id") Long id) {
  User user = new User(id, "Name-" + id, "123456-" + id, "1111111222222-" + id) ;
  return user ;
}
</code>

2.2 Custom ResponseBodyAdvice

This approach processes the response without creating proxies, offering a slight performance gain.

<code>@ControllerAdvice
public class SensitiveResponseBodyAdvice implements ResponseBodyAdvice<Object> {
  @Resource
  private SensitivePropsProcessor sensitivePropsProcessor ;

  @Override
  public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
    Class<?> parameterType = returnType.getParameterType() ;
    if (parameterType.isPrimitive()) {
      return false ;
    }
    return returnType.getMethodAnnotation(Sensitive.class) != null ;
  }

  @Override
  public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
      Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
      ServerHttpResponse response) {
    String[] props = returnType.getMethodAnnotation(Sensitive.class).value() ;
    this.sensitivePropsProcessor.processor(body, List.of(props)) ;
    return body ;
  }
}
</code>

Both implementations rely on the same SecretProcessor and SensitivePropsProcessor components shown earlier.

The AOP method encrypts fields after the target method returns, while the ResponseBodyAdvice method encrypts fields just before the response body is written, avoiding proxy creation.

Choose the approach that best fits your performance and architectural requirements.

JavaAOPSpringBootEncryptionResponseBodyAdvice
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.