Implementing Request and Response Encryption in Spring Boot Using ControllerAdvice and Jackson
This article explains how to secure Spring Boot APIs by encrypting request and response bodies with symmetric AES, handling signatures, and configuring Jackson serialization to preserve enum and date formats, providing complete code examples and debugging insights.
In this article a senior architect demonstrates how to secure API interfaces in a Spring Boot backend by encrypting both request and response bodies.
The requirements include minimal impact on existing business logic, symmetric AES encryption, separate keys for H5 and mobile clients, and support for both GET and POST endpoints.
First, data models such as @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; } and an enum @Getter public enum UserType { VIP("VIP用户"), COMMON("普通用户"); private String code; private String type; UserType(String type) { this.code = name(); this.type = type; } are defined.
A simple controller @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; } } returns a list of users.
To intercept and decrypt incoming requests, a @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 { /* signature verification and AES decryption logic */ } } is implemented, which reads the encrypted body, verifies signatures, and performs AES decryption.
For outgoing responses, @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; } } encrypts the serialized JSON, adds a timestamp, salt and signature, and returns a standardized encrypted wrapper.
Initial testing shows correct encryption/decryption, but serialization issues appear: the enum and LocalDateTime fields are not formatted as expected when using FastJSON.
Switching to Jackson resolves the problem; the code is changed to String data = new ObjectMapper().writeValueAsString(o); and a custom ObjectMapper with LocalDateTimeSerializer and LocalDateTimeDeserializer is configured to use the pattern "yyyy-MM-dd HH:mm:ss" :
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();
Finally, the SecretResponseAdvice is refactored to autowire the Spring‑managed ObjectMapper , ensuring that the encrypted response matches the format of the non‑encrypted version across all endpoints:
@Autowired private ObjectMapper objectMapper; ... String dataStr = objectMapper.writeValueAsString(o); String data = EncryptUtils.aesEncrypt(dataStr, secretKey);
The article also includes request/response examples, debugging steps, and references to related resources.
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.
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.