Design and Implementation of a Spring Boot Starter for Request/Response Encryption and Decryption
This article explains how to build a reusable Spring Boot starter that automatically encrypts outgoing responses and decrypts incoming requests using hutool‑crypto, custom request wrappers, validation utilities, and Spring Boot advice components, providing a secure, zero‑boilerplate solution for Java backend services.
1. Introduction In typical Java development, services often exchange data with other systems or micro‑services, and protecting that data requires encrypting request parameters and decrypting responses. A generic Spring Boot starter can encapsulate this functionality to avoid repetitive code.
2. Prerequisite Knowledge
hutool‑crypto offers a variety of encryption tools (symmetric, asymmetric, digest, etc.).
Request streams in Servlet APIs can be read only once; to reuse them, a wrapper that copies the input stream is needed.
Spring Boot validation (SpringBoot‑validation) allows declarative parameter checks via annotations such as @Validated or @Valid , throwing BindException on failure.
Creating a custom starter involves writing functional code, declaring an auto‑configuration class, and registering it in META-INF/spring.factories .
RequestBodyAdvice and ResponseBodyAdvice can intercept JSON payloads for automatic decryption and encryption respectively.
3. Feature Overview The starter encrypts response data and decrypts request data transparently. Encrypted data uses AES (via hutool‑crypto) and includes a timestamp field inherited from a common base class to enforce a 60‑minute validity window.
4. Detailed Functionality
Encryption uses symmetric AES with parameters defined in crypto.properties (mode, padding, key, iv).
When a controller method is annotated with @EncryptionAnnotation , the response body (wrapped in a unified Result object) is encrypted automatically.
When a method is annotated with @DecryptionAnnotation and the request body follows the RequestData format, the incoming JSON is decrypted, validated, and the timestamp is checked.
All payload objects must extend RequestBase to carry the timestamp.
5. Code Implementation
5.1 Project Structure (illustrated with diagrams in the original article).
5.2 crypto‑common
package xyz.hlh.crypto.common;
// (module containing shared utilities and constants)5.3 crypto‑spring‑boot‑starter
5.3.1 Configuration Files
# crypto.properties – AES parameters
crypto.mode=CTS
crypto.padding=PKCS5Padding
crypto.key=testkey123456789
crypto.iv=testiv1234567890 # spring.factories – auto‑configuration registration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
xyz.hlh.crypto.config.AppConfig5.3.2 Core Configuration Classes
package xyz.hlh.crypto.config;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.Serializable;
/** AES configuration parameters */
@Configuration
@ConfigurationProperties(prefix = "crypto")
@PropertySource("classpath:crypto.properties")
@Data
@EqualsAndHashCode
@Getter
public class CryptConfig implements Serializable {
private Mode mode;
private Padding padding;
private String key;
private String iv;
} package xyz.hlh.crypto.config;
import cn.hutool.crypto.symmetric.AES;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
@Configuration
public class AppConfig {
@Resource
private CryptConfig cryptConfig;
@Bean
public AES aes() {
return new AES(
cryptConfig.getMode(),
cryptConfig.getPadding(),
cryptConfig.getKey().getBytes(StandardCharsets.UTF_8),
cryptConfig.getIv().getBytes(StandardCharsets.UTF_8)
);
}
}5.4 Request/Response Advice
package xyz.hlh.crypto.advice;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import xyz.hlh.crypto.annotation.DecryptionAnnotation;
import xyz.hlh.crypto.common.exception.ParamException;
import xyz.hlh.crypto.constant.CryptoConstant;
import xyz.hlh.crypto.entity.RequestBase;
import xyz.hlh.crypto.entity.RequestData;
import xyz.hlh.crypto.util.AESUtil;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Type;
/** Automatic request body decryption */
@ControllerAdvice
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class
> converterType) {
return methodParameter.hasMethodAnnotation(DecryptionAnnotation.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class
> converterType) throws IOException {
return inputMessage;
}
@SneakyThrows
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class
> converterType) {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) attrs;
if (sra == null) {
throw new ParamException("request错误");
}
HttpServletRequest request = sra.getRequest();
ServletInputStream is = request.getInputStream();
RequestData requestData = objectMapper.readValue(is, RequestData.class);
if (requestData == null || StringUtils.isBlank(requestData.getText())) {
throw new ParamException("参数错误");
}
String text = requestData.getText();
request.setAttribute(CryptoConstant.INPUT_ORIGINAL_DATA, text);
String decryptText = AESUtil.decrypt(text);
if (StringUtils.isBlank(decryptText)) {
throw new ParamException("解密失败");
}
request.setAttribute(CryptoConstant.INPUT_DECRYPT_DATA, decryptText);
Object result = objectMapper.readValue(decryptText, body.getClass());
if (result instanceof RequestBase) {
Long ts = ((RequestBase) result).getCurrentTimeMillis();
long effective = 60 * 1000L;
if (Math.abs(System.currentTimeMillis() - ts) > effective) {
throw new ParamException("时间戳不合法");
}
return result;
} else {
throw new ParamException(String.format("请求参数类型:%s 未继承:%s", result.getClass().getName(), RequestBase.class.getName()));
}
}
@SneakyThrows
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class
> converterType) {
Class
cls = Class.forName(targetType.getTypeName());
return cls.newInstance();
}
} package xyz.hlh.crypto.advice;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import xyz.hlh.crypto.annotation.EncryptionAnnotation;
import xyz.hlh.crypto.common.entity.Result;
import xyz.hlh.crypto.common.exception.CryptoException;
import xyz.hlh.crypto.entity.RequestBase;
import xyz.hlh.crypto.util.AESUtil;
import java.lang.reflect.Type;
/** Automatic response body encryption */
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice
> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class
> converterType) {
// Simplified check: if method has @EncryptionAnnotation and returns Result
return returnType.hasMethodAnnotation(EncryptionAnnotation.class);
}
@SneakyThrows
@Override
public Result
beforeBodyWrite(Result
body, MethodParameter returnType, MediaType selectedContentType,
Class
> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
Object data = body.getData();
if (data == null) {
return body;
}
if (data instanceof RequestBase) {
((RequestBase) data).setCurrentTimeMillis(System.currentTimeMillis());
}
String json = JSONUtil.toJsonStr(data);
if (StringUtils.isBlank(json) || json.length() < 16) {
throw new CryptoException("加密失败,数据小于16位");
}
String encrypt = AESUtil.encryptHex(json);
return Result.builder()
.status(body.getStatus())
.data(encrypt)
.message(body.getMessage())
.build();
}
}5.5 Example Entities and Controllers
package xyz.hlh.crypto.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
/** Teacher entity with validation */
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Teacher extends RequestBase implements Serializable {
@NotBlank(message = "姓名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
@Range(min = 0, max = 150, message = "年龄不合法")
private Integer age;
@NotNull(message = "生日不能为空")
private Date birthday;
} package xyz.hlh.crypto.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import xyz.hlh.crypto.annotation.DecryptionAnnotation;
import xyz.hlh.crypto.annotation.EncryptionAnnotation;
import xyz.hlh.crypto.common.entity.Result;
import xyz.hlh.crypto.common.entity.ResultBuilder;
import xyz.hlh.crypto.entity.Teacher;
@RestController
public class TestController implements ResultBuilder {
@PostMapping("/get")
public ResponseEntity
> get(@Validated @RequestBody Teacher teacher) {
return success(teacher);
}
@PostMapping("/encrypt")
@EncryptionAnnotation
public ResponseEntity
> encrypt(@Validated @RequestBody Teacher teacher) {
return success(teacher);
}
@PostMapping("/encrypt1")
@EncryptionAnnotation
public Result
encrypt1(@Validated @RequestBody Teacher teacher) {
return success(teacher).getBody();
}
@PostMapping("/decrypt")
@DecryptionAnnotation
public ResponseEntity
> decrypt(@Validated @RequestBody Teacher teacher) {
return success(teacher);
}
}The article concludes with a disclaimer that the content is sourced from the internet for learning purposes only.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.