Backend Development 21 min read

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.

Architect's Guide
Architect's Guide
Architect's Guide
Design and Implementation of a Spring Boot Starter for Request/Response Encryption and Decryption

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.AppConfig

5.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.

BackendJavaSpring BootencryptionResponseBodyAdviceStarterRequestBodyAdvice
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

login 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.