Backend Development 13 min read

Implementing Request and Response Encryption in Spring Boot with ControllerAdvice and Jackson

This article demonstrates how to secure Spring Boot APIs by encrypting request and response bodies using custom ControllerAdvice, switching from FastJson to Jackson for proper enum and date serialization, and configuring ObjectMapper to produce consistent JSON output across encrypted and non‑encrypted endpoints.

Architect's Guide
Architect's Guide
Architect's Guide
Implementing Request and Response Encryption in Spring Boot with ControllerAdvice and Jackson

A developer is tasked with adding encryption to a Spring Boot service that must support Android, iOS, and H5 clients while minimizing code changes and preserving existing business logic.

Requirements:

Minimal impact on existing code.

Symmetric encryption for quick implementation.

Separate keys for H5 and mobile clients.

Compatibility with legacy interfaces; new interfaces need not be backward compatible.

Both GET and POST endpoints must be encrypted/decrypted.

The solution uses ControllerAdvice to intercept requests and responses. The request interceptor ( SecretRequestAdvice ) reads the raw body, verifies required headers (clientType, timestamp, salt, signature), decrypts the payload with AES, and passes the plaintext to the controller.

@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class SecretRequestAdvice extends RequestBodyAdviceAdapter {
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class
> aClass) {
        return true;
    }
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class
> converterType) throws IOException {
        String httpBody;
        if (Boolean.TRUE.equals(SecretFilter.secretThreadLocal.get())) {
            httpBody = decryptBody(inputMessage);
        } else {
            httpBody = StreamUtils.copyToString(inputMessage.getBody(), Charset.defaultCharset());
        }
        return new SecretHttpMessage(new ByteArrayInputStream(httpBody.getBytes()), inputMessage.getHeaders());
    }
    private String decryptBody(HttpInputMessage inputMessage) throws IOException {
        // header validation and AES decryption logic
        // ...
    }
}

The response interceptor ( SecretResponseAdvice ) encrypts the controller's return object, adds a timestamp, salt, and signature, and returns a wrapped response.

@ControllerAdvice
public class SecretResponseAdvice implements ResponseBodyAdvice
{
    private Logger logger = LoggerFactory.getLogger(SecretResponseAdvice.class);
    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) { return true; }
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest request, ServerHttpResponse response) {
        Boolean respSecret = SecretFilter.secretThreadLocal.get();
        String secretKey = SecretFilter.clientPrivateKeyThreadLocal.get();
        SecretFilter.secretThreadLocal.remove();
        SecretFilter.clientPrivateKeyThreadLocal.remove();
        if (respSecret != null && respSecret) {
            if (o instanceof ResponseBasic) {
                if (SECRET_API_ERROR == ((ResponseBasic) o).getCode()) {
                    return SecretResponseBasic.fail(((ResponseBasic) o).getCode(), ((ResponseBasic) o).getData(), ((ResponseBasic) o).getMsg());
                }
                try {
                    String data = EncryptUtils.aesEncrypt(JSON.toJSONString(o), secretKey);
                    long timestamp = System.currentTimeMillis() / 1000;
                    int salt = EncryptUtils.genSalt();
                    String newSignature = Md5Utils.genSignature(timestamp + "" + salt + "" + data + secretKey);
                    return SecretResponseBasic.success(data, timestamp, salt, newSignature);
                } catch (Exception e) {
                    logger.error("beforeBodyWrite error:", e);
                    return SecretResponseBasic.fail(SECRET_API_ERROR, "", "服务端处理结果数据异常");
                }
            }
        }
        return o;
    }
}

Initially FastJson was used for serialization, but it produced incorrect enum and date formats after encryption. Switching to Jackson solved the problem. The response advice was updated to use an injected ObjectMapper for serialization:

@Autowired
private ObjectMapper objectMapper;
// inside beforeBodyWrite
String dataStr = objectMapper.writeValueAsString(o);
String data = EncryptUtils.aesEncrypt(dataStr, secretKey);

To keep date formatting consistent with the non‑encrypted version, a custom ObjectMapper was built with LocalDateTimeSerializer and LocalDateTimeDeserializer using the pattern yyyy-MM-dd HH:mm:ss :

String DATE_TIME_FORMATTER = "yyyy-MM-dd HH:mm:ss";
ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder()
    .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER)))
    .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER)))
    .build();

After these changes, encrypted responses match the original API output, including proper enum representation ( {"code":"COMMON","type":"普通用户"} ) and correctly formatted timestamps (e.g., 2022-03-29 22:57:33 ).

The article also notes remaining considerations such as handling different date formats across services and potential cross‑origin issues for GET requests.

JavaSpring BootJacksonEncryptionAPI securityControllerAdvice
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.