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.
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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.