Implementing a Spring Boot Encryption/Decryption Starter with Hutool-Crypto
This article explains how to build a reusable Spring Boot starter that automatically encrypts response data and decrypts request payloads using Hutool-Crypto's AES utilities, covering request stream handling, validation, custom starter configuration, and example code for controllers and entities.
Introduction: In typical Java micro‑service development, secure data transmission is required; the article proposes a reusable Spring Boot starter that provides automatic encryption and decryption to avoid repetitive code.
Prerequisite knowledge: hutool‑crypto encryption tools, the single‑read limitation of HttpServletRequest streams, Spring Boot validation (validation annotations), custom starter creation, and the use of RequestBodyAdvice and ResponseBodyAdvice for request/response processing.
Solution for request stream: a wrapper class caches the request body by extending HttpServletRequestWrapper and overrides getInputStream and getReader to return a copy, while a filter replaces the original request with this wrapper.
package xyz.hlh.cryptotest.utils;
import org.apache.commons.io.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
/**
* Request stream supporting multiple reads
*/
public class InputStreamHttpServletRequestWrapper extends HttpServletRequestWrapper {
private ByteArrayOutputStream cachedBytes;
public InputStreamHttpServletRequestWrapper(HttpServletRequest request) { super(request); }
@Override
public ServletInputStream getInputStream() throws IOException {
if (cachedBytes == null) { cacheInputStream(); }
return new CachedServletInputStream(cachedBytes.toByteArray());
}
@Override
public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); }
private void cacheInputStream() throws IOException { cachedBytes = new ByteArrayOutputStream(); IOUtils.copy(super.getInputStream(), cachedBytes); }
public static class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream input;
public CachedServletInputStream(byte[] buf) { input = new ByteArrayInputStream(buf); }
@Override public boolean isFinished() { return false; }
@Override public boolean isReady() { return false; }
@Override public void setReadListener(ReadListener listener) {}
@Override public int read() throws IOException { return input.read(); }
}
} package xyz.hlh.cryptotest.filter;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import xyz.hlh.cryptotest.utils.InputStreamHttpServletRequestWrapper;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
@Component
@Order(HIGHEST_PRECEDENCE + 1)
public class HttpServletRequestInputStreamFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
InputStreamHttpServletRequestWrapper wrapper = new InputStreamHttpServletRequestWrapper(httpServletRequest);
chain.doFilter(wrapper, response);
}
}Validation utilities: a custom ParamException and ValidationUtils class use the JSR‑380 validator to check bean constraints and throw detailed exceptions.
package xyz.hlh.cryptotest.exception;
import lombok.Getter;
import java.util.List;
@Getter
public class ParamException extends Exception {
private final List
fieldList;
private final List
msgList;
public ParamException(List
fieldList, List
msgList) { this.fieldList = fieldList; this.msgList = msgList; }
} package xyz.hlh.cryptotest.utils;
import javax.validation.*;
import java.util.*;
public class ValidationUtils {
private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
public static void validate(Object object) throws CustomizeException {
Set
> validate = VALIDATOR.validate(object);
throwParamException(validate);
}
private static void throwParamException(Set
> validate) throws CustomizeException {
if (validate.size() > 0) {
List
fieldList = new LinkedList<>();
List
msgList = new LinkedList<>();
for (ConstraintViolation
next : validate) {
fieldList.add(next.getPropertyPath().toString());
msgList.add(next.getMessage());
}
throw new ParamException(fieldList, msgList);
}
}
}Custom starter creation steps: create a factory with functional code, declare an auto‑configuration class, and register it in META-INF/spring.factories using the key org.springframework.boot.autoconfigure.EnableAutoConfiguration .
Functionality: when a controller method is annotated with @DecryptionAnnotation , DecryptRequestBodyAdvice reads the encrypted JSON wrapped in RequestData , decrypts it with AESUtil , validates the timestamp, and converts it to the target object. When a method returns a Result annotated with @EncryptionAnnotation , EncryptResponseBodyAdvice adds a timestamp, serializes the data, encrypts it, and wraps it back into Result .
Key configuration files:
# crypto.properties
crypto.mode=CTS
crypto.padding=PKCS5Padding
crypto.key=testkey123456789
crypto.iv=testiv1234567890 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
xyz.hlh.crypto.config.AppConfigCore auto‑configuration classes:
package xyz.hlh.crypto.config;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.Serializable;
@Configuration
@ConfigurationProperties(prefix = "crypto")
@PropertySource("classpath:crypto.properties")
@Data
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));
}
}RequestBodyAdvice implementation decrypts incoming payloads, checks the timestamp (valid for 60 seconds), and returns the deserialized object.
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.*;
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.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Type;
@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();
RequestData requestData = objectMapper.readValue(request.getInputStream(), 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 currentTimeMillis = ((RequestBase) result).getCurrentTimeMillis();
long effective = 60 * 1000;
long expire = System.currentTimeMillis() - currentTimeMillis;
if (Math.abs(expire) > 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
bodyClass = Class.forName(targetType.getTypeName());
return bodyClass.newInstance();
}
}ResponseBodyAdvice implementation encrypts the data field of a Result , adds a timestamp if the data extends RequestBase , and returns the encrypted JSON string.
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.*;
import org.springframework.http.converter.HttpMessageConverter;
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;
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice
> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class
> converterType) {
// Simplified check for @EncryptionAnnotation on methods returning 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 dataText = JSONUtil.toJsonStr(data);
if (StringUtils.isBlank(dataText)) { return body; }
if (dataText.length() < 16) { throw new CryptoException("加密失败,数据小于16位"); }
String encryptText = AESUtil.encryptHex(dataText);
return Result.builder()
.status(body.getStatus())
.data(encryptText)
.message(body.getMessage())
.build();
}
}Example entity Teacher extends RequestBase and uses validation annotations for name, age, and birthday.
package xyz.hlh.crypto.entity;
import lombok.*;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.*;
import java.io.Serializable;
import java.util.Date;
@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;
}TestController demonstrates four endpoints: a plain /get returning the object, /encrypt and /encrypt1 returning encrypted results, and /decrypt which expects encrypted input and returns the decrypted object.
package xyz.hlh.crypto.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import xyz.hlh.crypto.annotation.*;
import xyz.hlh.crypto.common.entity.Result;
import xyz.hlh.crypto.entity.Teacher;
@RestController
public class TestController {
@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 also includes diagrams of the project structure and references to further reading on micro‑service architecture.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.