How to Secure Spring MVC APIs with Unified Request/Response Encryption
This article walks through implementing symmetric encryption for both request and response bodies in a Spring MVC project, covering requirement analysis, data model design, custom ControllerAdvice for decryption and encryption, serialization challenges with FastJson and Jackson, and final configuration to ensure consistent API output across Android, iOS, and H5 clients.
After being assigned a security task, the author outlines the requirements: minimal code changes, symmetric encryption for both GET and POST interfaces, separate keys for H5 and mobile clients, and compatibility with legacy APIs.
Minimize impact on existing business logic.
Use symmetric encryption; provide two key sets for H5 and Android/iOS.
Support both GET and POST; new interfaces need not be backward compatible.
The data model includes a User class and a UserType enum annotated with @JsonFormat. A simple UserController returns a list of users.
@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+"\"}"; }
}The encryption flow is handled by two ControllerAdvice classes. SecretRequestAdvice intercepts incoming requests, validates headers, verifies signatures, and decrypts the body using AES.
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class SecretRequestAdvice extends RequestBodyAdviceAdapter {
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { return true; }
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> 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 omitted for brevity
String requestBody = StreamUtils.copyToString(inputMessage.getBody(), Charset.defaultCharset());
// signature verification omitted
String privateKey = SecretFilter.clientPrivateKeyThreadLocal.get();
ReqSecret reqSecret = JSON.parseObject(requestBody, ReqSecret.class);
String data = reqSecret.getData();
String newSignature = StringUtils.isEmpty(privateKey) ? "" : Md5Utils.genSignature(reqSecret.getTimestamp()+reqSecret.getSalt()+data+privateKey);
if (!newSignature.equals(reqSecret.getSignature())) { throw new ResultException(SECRET_API_ERROR, "Signature verification failed"); }
try {
String decrypt = EncryptUtils.aesDecrypt(data, privateKey);
return StringUtils.isEmpty(decrypt) ? "{}" : decrypt;
} catch (Exception e) { log.error("error: ", e); }
throw new ResultException(SECRET_API_ERROR, "Decryption failed");
}
} SecretResponseAdviceencrypts the response, adds a timestamp, a random salt, and a new signature before sending it back.
@ControllerAdvice
public class SecretResponseAdvice implements ResponseBodyAdvice<Object> {
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, "", "Service result data error");
}
}
}
return o;
}
}During testing, the author discovered that FastJson serialized the UserType enum and LocalDateTime fields incorrectly, causing mismatched formats after encryption. Attempts with SerializerFeature.WriteEnumUsingToString and custom toString did not produce the desired JSON structure.
Switching to Jackson solved the enum issue, but the default LocalDateTime serialization produced a verbose object. By configuring a custom ObjectMapper with LocalDateTimeSerializer and LocalDateTimeDeserializer using the pattern yyyy-MM-dd HH:mm:ss, the author achieved the same output as the non‑encrypted version.
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 was updated to use the injected ObjectMapper for serialization before encryption, ensuring that the encrypted response matches the original API contract across all client platforms.
Source: https://github.com/boykait/encrypt-demo Original article: juejin.cn/post/7080568585021554718
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.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.
