Implementing API Request and Response Encryption in Spring Boot with ControllerAdvice
This article walks through a complete Spring Boot solution for encrypting and decrypting API requests and responses, covering requirement analysis, data models, custom ControllerAdvice implementations, serialization challenges, and practical code examples using AES and Jackson.
Hello everyone, I'm the author and this post discusses API interface security, focusing on request/response encryption and decryption.
The main requirements are: minimal impact on existing business logic; use symmetric encryption with separate keys for H5 and Android/iOS due to key storage concerns; maintain compatibility with legacy low‑version interfaces; and support both GET and POST encryption.
To meet these needs we unify interception on the server, client, and H5 sides, adopt AES encryption, and prefix new encrypted endpoints with /secret/ .
Data models:
@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;
}
}A simple user‑list controller example:
@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;
}
}Calling localhost:8080/user/list returns the expected JSON payload.
Encryption is handled via two @ControllerAdvice classes. The request decryption advice:
@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 {
// verify headers, signature, then AES decrypt
// ... (omitted for brevity)
throw new ResultException(SECRET_API_ERROR, "解密失败");
}
}The response encryption advice:
@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;
}
}Testing shows the full flow: client encrypts request → server decrypts → business logic → server encrypts response → client decrypts.
During integration a mismatch appeared: the userType enum and registerTime fields were not serialized as expected. The issue stemmed from using FastJson's JSON.toJSONString . Switching to Jackson's ObjectMapper resolved the enum problem:
String data = EncryptUtils.aesEncrypt(new ObjectMapper().writeValueAsString(o), secretKey);However, Jackson serialized LocalDateTime into a verbose object. Configuring the mapper with a custom serializer/deserializer for the desired pattern fixed the 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 this configuration the encrypted response matches the non‑encrypted version exactly.
To avoid hard‑coding the mapper, the advice can autowire Spring's configured ObjectMapper :
@ControllerAdvice
public class SecretResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest request, ServerHttpResponse response) {
// ...
String dataStr = objectMapper.writeValueAsString(o);
String data = EncryptUtils.aesEncrypt(dataStr, secretKey);
// ...
}
}The article concludes with a reminder to like, share, and follow the author, and includes a promotional note about a knowledge‑sharing community offering Spring‑related courses.
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
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.