Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Custom Spring Boot Starter for Global API Encryption

Tech May 7 4

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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.