Information Security 14 min read

Implementing API Request and Response Encryption/Decryption with Spring MVC and ControllerAdvice

This article demonstrates how to secure API interfaces by implementing symmetric encryption and decryption for both GET and POST requests in a Spring MVC application, using ControllerAdvice to intercept and process request and response bodies, handling key management, signature verification, and serialization issues across H5, Android, and iOS clients.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing API Request and Response Encryption/Decryption with Spring MVC and ControllerAdvice

The article discusses the need for API interface security, outlining requirements such as minimal impact on existing business logic, using symmetric encryption for Android, iOS, and H5 clients, separate keys for H5, and supporting both GET and POST methods.

Two domain objects are defined: a @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 @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 + "\"}"; } } .

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 user list at localhost:8080/user/list , producing a normal JSON response.

To secure the API, @ControllerAdvice 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等参数传递是否正确传递"); } String timestamp = String.valueOf(Objects.requireNonNull(headers.get("timestamp")).get(0)); String salt = String.valueOf(Objects.requireNonNull(headers.get("salt")).get(0)); String signature = String.valueOf(Objects.requireNonNull(headers.get("signature")).get(0)); String privateKey = SecretFilter.clientPrivateKeyThreadLocal.get(); ReqSecret reqSecret = JSON.parseObject(requestBody, ReqSecret.class); String data = reqSecret.getData(); String newSignature = ""; if (!StringUtils.isEmpty(privateKey)) { newSignature = Md5Utils.genSignature(timestamp + salt + data + privateKey); } if (!newSignature.equals(signature)) { throw new ResultException(SECRET_API_ERROR, "验签失败,请确认加密方式是否正确"); } try { String decrypt = EncryptUtils.aesDecrypt(data, privateKey); if (StringUtils.isEmpty(decrypt)) { decrypt = "{}"; } return decrypt; } catch (Exception e) { log.error("error: ", e); } throw new ResultException(SECRET_API_ERROR, "解密失败"); } } handles request decryption, while @ControllerAdvice public class SecretResponseAdvice implements ResponseBodyAdvice { @Autowired private ObjectMapper objectMapper; @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 dataStr = objectMapper.writeValueAsString(o); String data = EncryptUtils.aesEncrypt(dataStr, secretKey); long timestamp = System.currentTimeMillis() / 1000; int salt = EncryptUtils.genSalt(); String dataNew = timestamp + "" + salt + "" + data + secretKey; String newSignature = Md5Utils.genSignature(dataNew); 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 response.

Initial testing revealed serialization mismatches for the UserType enum and LocalDateTime fields. Switching from FastJSON to Jackson resolved the enum issue, and configuring Jackson with a custom ObjectMapper serializer/deserializer for LocalDateTime restored the original date format:

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();

After these adjustments, encrypted responses matched the non‑encrypted version, and both GET and POST encryption scenarios worked across H5, Android, and iOS clients. The article concludes with a note that further topics such as cross‑origin issues and additional edge cases will be explored later.

Javabackend developmentEncryptioninformation securityAPI securitySpring MVCControllerAdvice
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.