Securing Spring Boot APIs with Request/Response Encryption via ControllerAdvice

This article walks through implementing symmetric request and response encryption for Spring Boot APIs—covering requirements, data models, controller examples, debugging serialization issues, and using ControllerAdvice with Jackson to ensure encrypted payloads remain compatible across Android, iOS, and H5 clients.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Securing Spring Boot APIs with Request/Response Encryption via ControllerAdvice

The author is tasked with adding encryption to all API interfaces of a Java project, aiming for minimal impact on existing business logic, symmetric encryption, separate keys for H5 versus mobile clients, and support for both GET and POST requests.

Key requirements include:

Minimal changes to existing code.

Use symmetric encryption; provide two key sets—one for H5 and one for Android/iOS.

Maintain compatibility with legacy interfaces; new interfaces need not be backward compatible.

Encrypt both GET and POST request bodies.

The solution uses a unified interceptor pattern: a SecretRequestAdvice to decrypt incoming requests, a SecretResponseAdvice to encrypt outgoing responses, and a custom filter for GET‑type decryption.

Data model definitions:

@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 + "\"}";
    }
}

A simple controller demonstrates the API:

@RestController
@RequestMapping({"/user", "/secret/user"})
public class UserController {
    @RequestMapping("/list")
    public ResponseEntity<List<User>> listUser() {
        List<User> 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<List<User>> response = new ResponseEntity<>();
        response.setCode(200);
        response.setData(users);
        response.setMsg("用户列表查询成功");
        return response;
    }
}

Calling localhost:8080/secret/user/list with appropriate headers ( signature, timestamp, salt, clientType) returns an encrypted JSON payload; after decryption the structure matches the original response.

During debugging the author discovered that FastJson’s default serialization produced unexpected enum and date formats. By switching to Jackson’s ObjectMapper and configuring LocalDateTimeSerializer / LocalDateTimeDeserializer with the pattern yyyy-MM-dd HH:mm:ss, the output aligned with the non‑encrypted version.

The final SecretResponseAdvice injects the configured ObjectMapper and serializes the response object before encryption:

@ControllerAdvice
public class SecretResponseAdvice implements ResponseBodyAdvice<Object> {
    @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<? extends HttpMessageConverter<?>> converterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        Boolean respSecret = SecretFilter.secretThreadLocal.get();
        String secretKey = SecretFilter.clientPrivateKeyThreadLocal.get();
        SecretFilter.secretThreadLocal.remove();
        SecretFilter.clientPrivateKeyThreadLocal.remove();
        if (Boolean.TRUE.equals(respSecret) && o instanceof ResponseBasic) {
            try {
                String dataStr = objectMapper.writeValueAsString(o);
                String data = EncryptUtils.aesEncrypt(dataStr, 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;
    }
}

After these adjustments the encrypted response is identical to the original format, and the author notes remaining work on GET‑request encryption and CORS handling. The full demo is available at the provided GitHub repository.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javaSpring BootJacksonAESControllerAdviceapi-encryption
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

0 followers
Reader feedback

How this landed with the community

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.