Backend Development 15 min read

Implementing Request and Response Encryption in Spring MVC Using ControllerAdvice

This article demonstrates how to secure GET and POST API endpoints in a Spring MVC application by encrypting request bodies and responses with AES, using custom ControllerAdvice classes, handling serialization issues with FastJSON and Jackson, and configuring ObjectMapper for proper date and enum formatting.

Top Architect
Top Architect
Top Architect
Implementing Request and Response Encryption in Spring MVC Using ControllerAdvice

In this tutorial a senior architect explains how to add end‑to‑end encryption for both request and response payloads in a Spring MVC project, covering requirements such as minimal impact on existing business logic, symmetric encryption for Android, iOS and H5 clients, and separate keys for the H5 side.

First, a simple User class and a UserType enum are defined, followed by a UserController that returns a list of users under the paths /user/list and /secret/user/list . The controller builds a ResponseEntity with a success code, data and message.

@Data
public class User {
    private Integer id;
    private String name;
    private UserType userType = UserType.COMMON;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime registerTime;
}
@Getter
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum UserType {
    VIP("VIP用户"),
    COMMON("普通用户");
    private String code;
    private String type;
    UserType(String type) {
        this.code = name();
        this.type = type;
    }
    @Override
    public String toString() {
        return "{\"code\":\"" + name() + "\", \"type\":\"" + type + "\"}";
    }
}
@RestController
@RequestMapping({"/user", "/secret/user"})
public class UserController {
    @RequestMapping("/list")
    ResponseEntity
> listUser() {
        List
users = new ArrayList<>();
        User u = new User();
        u.setId(1);
        u.setName("boyka");
        u.setRegisterTime(LocalDateTime.now());
        u.setUserType(UserType.COMMON);
        users.add(u);
        ResponseEntity
> response = new ResponseEntity<>();
        response.setCode(200);
        response.setData(users);
        response.setMsg("用户列表查询成功");
        return response;
    }
}

To decrypt incoming requests, a SecretRequestAdvice extending RequestBodyAdviceAdapter is created. It checks a thread‑local flag, reads the raw body, validates required headers (clientType, timestamp, salt, signature), performs signature verification, and finally decrypts the payload using AES.

@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 {
        InputStream encryptStream = inputMessage.getBody();
        String requestBody = StreamUtils.copyToString(encryptStream, Charset.defaultCharset());
        HttpHeaders headers = inputMessage.getHeaders();
        if (CollectionUtils.isEmpty(headers.get("clientType")) || CollectionUtils.isEmpty(headers.get("timestamp")) || CollectionUtils.isEmpty(headers.get("salt")) || CollectionUtils.isEmpty(headers.get("signature"))) {
            throw new ResultException(SECRET_API_ERROR, "请求解密参数错误,clientType、timestamp、salt、signature等参数传递是否正确传递");
        }
        // signature verification and AES decryption logic omitted for brevity
        return decrypt;
    }
}

For encrypting responses, SecretResponseAdvice implements ResponseBodyAdvice . It retrieves the encryption flag and key from thread‑local storage, encrypts the JSON representation of the response body with AES, adds a timestamp, salt and signature, and returns a SecretResponseBasic wrapper.

@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 serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        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;
    }
}

During testing the author discovered that FastJSON’s default serialization produced an unexpected format for the UserType enum and the LocalDateTime field. By switching to Jackson’s ObjectMapper and configuring it with WriteEnumUsingToString and a custom date formatter ( yyyy-MM-dd HH:mm:ss ), the encrypted payload matched the original unencrypted response.

String DATE_TIME_FORMATTER = "yyyy-MM-dd HH:mm:ss";
ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder()
    .findModulesViaServiceLoader(true)
    .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER)))
    .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER)))
    .build();
String dataStr = objectMapper.writeValueAsString(o);
String data = EncryptUtils.aesEncrypt(dataStr, secretKey);

Finally, the SecretResponseAdvice is updated to autowire the configured ObjectMapper , ensuring that all responses—whether encrypted or not—are serialized consistently with the project's formatting rules.

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

The article concludes that with these adjustments the encrypted API behaves identically to the original implementation, and invites readers to explore the full source code via the author’s contact information.

JavaREST APIJacksonEncryptionSpring MVCAESControllerAdvice
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.