Building a Custom Spring Boot Starter for Global API Encryption
Spring Boot Starters simplify dependency management and configuration by bundling related libraries into cohesive modules. Creating custom starters enables teams to standardize cross-cutting concerns like security, logging, and data transformation across distributed systems.
Auto-Configuration Fundamentals
Spring Boot auto-configuration loads candidate components through service provider interfaces. Create the resource descriptor META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (or spring.factories for legacy versions):
com.example.crypto.CryptoAutoConfiguration
Alternatively, use the @AutoConfiguration annotation in a dedicated configuration class:
@Slf4j
@AutoConfiguration
@ConditionalOnProperty(prefix = "api.crypto", name = "enabled", havingValue = "true", matchIfMissing = true)
public class CryptoAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CryptoStrategy defaultCryptoProcessor() {
return new StandardEncryptionImpl();
}
@Bean
public RequestDecryptAdvice requestDecryptAdvice() {
return new RequestDecryptAdvice();
}
@Bean
public ResponseEncryptAdvice responseEncryptAdvice() {
return new ResponseEncryptAdvice();
}
}
Defining the Encryption Contract
Establish a strategy interface to allow algorithm pluggability:
public interface CryptoStrategy {
String encrypt(String plaintext);
String decrypt(String ciphertext);
void initialize(CryptoProperties properties);
}
Create a marker annotation to identify secured endpoints:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SecuredApi {
String strategy() default "defaultCryptoProcessor";
boolean decryptRequest() default true;
boolean encryptResponse() default true;
}
Entercepting Request Bodies
Implement RequestBodyAdvice to decrypt incoming payloads before deserialization:
@Slf4j
@RestControllerAdvice
public class RequestDecryptAdvice implements RequestBodyAdvice {
@Autowired
private ApplicationContext context;
@Override
public boolean supports(MethodParameter methodParam, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return methodParam.hasMethodAnnotation(SecuredApi.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType)
throws IOException {
SecuredApi annotation = parameter.getMethodAnnotation(SecuredApi.class);
String payload = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8);
log.debug("Intercepting encrypted request for method: {}", parameter.getMethod());
if (annotation != null && annotation.decryptRequest()) {
CryptoStrategy processor = context.getBean(annotation.strategy(), CryptoStrategy.class);
String decrypted = processor.decrypt(payload);
return new ModifiedHttpInputMessage(inputMessage.getHeaders(),
new ByteArrayInputStream(decrypted.getBytes(StandardCharsets.UTF_8)));
}
return inputMessage;
}
// Empty implementations for afterBodyRead and handleEmptyBody...
}
Intercepting Response Bodies
Similarly, implement ResponseBodyAdvice to encrypt outgoing data:
@Slf4j
@RestControllerAdvice
public class ResponseEncryptAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ApplicationContext context;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(SecuredApi.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
SecuredApi annotation = returnType.getMethodAnnotation(SecuredApi.class);
if (annotation != null && annotation.encryptResponse() && body != null) {
CryptoStrategy processor = context.getBean(annotation.strategy(), CryptoStrategy.class);
String content = (body instanceof String) ? (String) body : JsonUtils.toJson(body);
log.debug("Encrypting response for method: {}", returnType.getMethod());
return processor.encrypt(content);
}
return body;
}
}
Configuration Properties
Externalize cryptographic settings through type-safe configuration:
@Data
@ConfigurationProperties(prefix = "api.crypto")
public class CryptoProperties {
private boolean enabled = true;
private String algorithm = "SM2";
private String privateKey;
private String publicKey;
private String charset = "UTF-8";
}
Enable configuration properties in the auto-configuration class:
@EnableConfigurationProperties(CryptoProperties.class)
public class CryptoAutoConfiguration {
// ...
}
SM2 Implementation Example
Below demonstrates a SM2 (Chinese cryptographic standard) implementation. Note that SM2 instances must reuse the same key pair to avoid InvalidCipherTextException:
@Slf4j
@Component("sm2CryptoStrategy")
public class Sm2EncryptionStrategy implements CryptoStrategy {
private static volatile SM2 sm2Instance;
private final Object lock = new Object();
@Override
@PostConstruct
public void initialize(CryptoProperties properties) {
if (sm2Instance == null) {
synchronized (lock) {
if (sm2Instance == null) {
if (StringUtils.hasText(properties.getPrivateKey()) &&
StringUtils.hasText(properties.getPublicKey())) {
// Use configured keys
sm2Instance = SmUtil.sm2(properties.getPrivateKey(), properties.getPublicKey());
} else {
// Generate transient keys for testing
KeyPair pair = SecureUtil.generateKeyPair("SM2");
sm2Instance = SmUtil.sm2(pair.getPrivate().getEncoded(),
pair.getPublic().getEncoded());
log.warn("Generated transient SM2 keys. Configure permanent keys for production.");
}
}
}
}
}
@Override
public String encrypt(String plaintext) {
try {
return sm2Instance.encryptBase64(plaintext, KeyType.PublicKey);
} catch (Exception e) {
log.error("Encryption failed", e);
throw new CryptoOperationException("Unable to encrypt payload", e);
}
}
@Override
public String decrypt(String ciphertext) {
try {
return StrUtil.utf8Str(sm2Instance.decryptStr(ciphertext, KeyType.PrivateKey));
} catch (Exception e) {
log.error("Decryption failed", e);
throw new CryptoOperationException("Unable to decrypt payload", e);
}
}
}
Usage Integration
Consume the starter by adding the dependency:
<dependency>
<groupId>com.example</groupId>
<artifactId>secure-api-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
Apply the security annotation to controler methods:
@RestController
@RequestMapping("/api/v1")
public class PaymentController {
@SecuredApi(strategy = "sm2CryptoStrategy")
@PostMapping("/transactions")
public TransactionResponse processPayment(@RequestBody PaymentRequest request) {
// Request body is automatically decrypted
// Response body is automatically encrypted
return service.execute(request);
}
@SecuredApi(encryptResponse = false) // Decrypt only
@PostMapping("/webhooks")
public String receiveWebhook(@RequestBody String payload) {
return "processed";
}
}
Configure in application.yml:
api:
crypto:
enabled: true
algorithm: SM2
public-key: ${CRYPTO_PUBLIC_KEY}
private-key: ${CRYPTO_PRIVATE_KEY}
Extending the Framework
Developers can implement custom algorithms by creating new CryptoStrategy beans:
@Component("aesGcmStrategy")
public class AesGcmStrategy implements CryptoStrategy {
// Implementation for AES-256-GCM
}
Then reference the custom strategy in the annotation:
@SecuredApi(strategy = "aesGcmStrategy")
Limitations and Considerations
The RequestBodyAdvice approach processes only request bodies, making it suitable for POST/PUT/PATCH methods with JSON or XML payloads. GET request parameters or form-data require alternative approaches such as Servlet Filters or Interceptors for comprehensive coverage.