Still Sending Plaintext? Spring Boot’s RSA + AES Automatic API Decryption for Full‑Scale Security
This article explains why API data must be encrypted, compares RSA and AES, presents a hybrid RSA‑AES scheme, and walks through a complete Spring Boot implementation—including encryption utilities, request wrappers, interceptors, parameter resolvers, key management, signature validation, batch processing, caching, and best‑practice recommendations—to achieve zero‑intrusion, high‑performance API security.
Why API Encryption Is Essential
Many systems perform functional and performance testing but rarely verify whether data transmitted over the network is protected. Common risk scenarios include plaintext passwords during registration, payment parameters captured by packet sniffers, reverse‑engineered apps exposing request rules, and internal APIs being bulk‑called and tampered with. If an API relies on HTTP + plaintext JSON, a man‑in‑the‑middle can read the data without breaking the system. The security baseline is that intercepted data should be useless.
Why RSA + AES
RSA provides asymmetric encryption, solving the key‑distribution problem: the public key can be shared openly while the private key stays on the server, making it suitable for key exchange and signature verification. Its drawback is poor performance for large payloads. AES offers symmetric encryption with fast processing, mature algorithms, and suitability for bulk data, but requires a secure way to distribute the symmetric key. Combining the two gives a clear responsibility split: RSA encrypts the AES key, and AES encrypts the business data.
Overall Architecture
Encryption Tool Layer : unified RSA/AES operations.
Request Wrapper Layer : automatically decrypts the request body.
Interceptor Layer : centrally handles encrypted requests.
Parameter Resolver : supports field‑level automatic decryption.
Key Management Module : dynamic key generation and periodic rotation (e.g., via Redis).
Security Enhancement Module : signature verification, caching, and rate limiting.
Core Implementation Details
EncryptionUtil implements RSA key encryption/decryption and AES encrypt/decrypt methods:
package com.icoderoad.security.crypto;
@Component
public class EncryptionUtil {
/** Encrypt AES key with RSA public key */
public static String encryptAesKey(String aesKey, String rsaPublicKey) throws Exception {
PublicKey publicKey = getPublicKey(rsaPublicKey);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(aesKey.getBytes()));
}
/** AES encrypt business data */
public static String encryptData(String data, String aesKey) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey.getBytes(), "AES");
IvParameterSpec iv = new IvParameterSpec("0000000000000000".getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
return Base64.getEncoder().encodeToString(cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}
// RSA decryption of AES key and AES decryption of data are analogous
}EncryptedRequestWrapper reads the raw body, decrypts it using the RSA‑derived AES key, and supplies a new ServletInputStream:
public class EncryptedRequestWrapper extends HttpServletRequestWrapper {
private final String body;
public EncryptedRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.body = readBody(request);
}
@Override
public ServletInputStream getInputStream() throws IOException {
String decryptedBody = decrypt(body);
ByteArrayInputStream inputStream = new ByteArrayInputStream(decryptedBody.getBytes(StandardCharsets.UTF_8));
return new DelegatingServletInputStream(inputStream);
}
private String decrypt(String encryptedBody) {
JSONObject json = JSON.parseObject(encryptedBody);
String aesKey = EncryptionUtil.decryptAesKey(json.getString("encryptedKey"), getPrivateKey());
return EncryptionUtil.decryptData(json.getString("encryptedData"), aesKey);
}
private String getPrivateKey() { return System.getenv("RSA_PRIVATE_KEY"); }
}DecryptionInterceptor checks a custom header X-Encrypted and replaces the request with the wrapper:
public class DecryptionInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if ("true".equalsIgnoreCase(request.getHeader("X-Encrypted"))) {
EncryptedRequestWrapper wrapper = new EncryptedRequestWrapper(request);
RequestContextHolder.currentRequestAttributes()
.setAttribute("wrappedRequest", wrapper, RequestAttributes.SCOPE_REQUEST);
}
return true;
}
}EncryptedParameterResolver enables method‑level decryption via the @EncryptedParam annotation:
public class EncryptedParameterResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(EncryptedParam.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
String encryptedValue = webRequest.getParameter(parameter.getParameterName());
return EncryptionUtil.decryptData(encryptedValue, getAesKey());
}
private String getAesKey() { return System.getenv("AES_KEY"); }
}Sample controller demonstrates usage:
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody UserRegisterRequest request) {
return ResponseEntity.ok("注册成功");
}
@PostMapping("/login")
public ResponseEntity<String> login(@EncryptedParam String username,
@EncryptedParam String password) {
return ResponseEntity.ok("登录成功");
}
}Advanced Capabilities
Dynamic AES Key Management (Redis) generates a 256‑bit key, stores it per user, and rotates it every 24 hours:
public class KeyManagementService {
@Autowired private RedisTemplate<String, String> redisTemplate;
public String generateAesKey() {
byte[] key = new byte[32];
new SecureRandom().nextBytes(key);
return Base64.getEncoder().encodeToString(key);
}
public String getOrCreateKey(String userId) {
String key = "security:aes:" + userId;
return redisTemplate.opsForValue().get(key, k -> generateAesKey(), Duration.ofHours(24));
}
public void rotateKey(String userId) {
redisTemplate.opsForValue().set("security:aes:" + userId, generateAesKey(), Duration.ofHours(24));
}
}Signature Validation uses SHA‑256 with RSA to ensure request integrity:
public class SignatureValidator {
public boolean verify(String data, String sign, String publicKey) {
try {
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(EncryptionUtil.getPublicKey(publicKey));
verifier.update(data.getBytes(StandardCharsets.UTF_8));
return verifier.verify(Base64.getDecoder().decode(sign));
} catch (Exception e) {
return false;
}
}
}Batch Parallel Encryption processes a list of strings concurrently:
public class BatchEncryptionProcessor {
public List<String> encrypt(List<String> list, String aesKey) {
return list.parallelStream()
.map(v -> EncryptionUtil.encryptData(v, aesKey))
.collect(Collectors.toList());
}
}Cache Layer stores encrypted results for 30 minutes:
public class EncryptedCacheManager {
@Autowired private RedisTemplate<String, Object> redisTemplate;
public void cache(String key, String value) {
redisTemplate.opsForValue().set("encrypted:" + key, value, Duration.ofMinutes(30));
}
public String get(String key) {
Object val = redisTemplate.opsForValue().get("encrypted:" + key);
return val == null ? null : val.toString();
}
}Thread‑Pool Isolation configures a dedicated executor for encryption tasks:
@Configuration
public class EncryptionThreadPoolConfig {
@Bean("encryptionExecutor")
public Executor encryptionExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("crypto-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}Selective Field Encryption defines a set of sensitive fields and provides a utility to decide whether a field should be encrypted:
public class EncryptionPolicy {
private static final Set<String> SENSITIVE = Set.of("password", "idcard", "phone", "email", "bankcard");
public static boolean shouldEncrypt(String field) {
return SENSITIVE.contains(field.toLowerCase());
}
}Unified Exception Handling returns a generic error message for any decryption‑related exception:
@ControllerAdvice
public class EncryptionExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handle(Exception e) {
return ResponseEntity.badRequest().body("请求数据异常");
}
}Security Practice Recommendations
Never hard‑code the RSA private key in source code.
HTTPS alone is only a baseline; payload encryption is required.
Rotate AES keys regularly (e.g., daily) and store them securely.
Handle all decryption exceptions uniformly to avoid leaking internal details.
Monitor and alert on abnormal access patterns to encrypted APIs.
Conclusion
RSA + AES is not a novelty; the difficulty lies in integrating it so that business code remains untouched, the architecture stays clear, and operations are sustainable. The presented design achieves end‑to‑end encryption, zero intrusion for developers, performance comparable to plaintext, and extensibility for auditing and maintenance.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
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.
