Secure API Communication with AES+RSA Hybrid Encryption in Spring Boot
This article walks through the design and implementation of a robust API security solution using a hybrid AES‑RSA encryption architecture, covering threat analysis, algorithm selection, key exchange, signature verification, replay‑attack defenses, and a complete Spring Boot codebase with best‑practice recommendations.
Threat Analysis
The main API security threats are:
Eavesdropping : Network sniffers can capture plaintext data.
Tampering : An attacker can modify fields, e.g., change an amount from 100 ¥ to 10 000 ¥.
Spoofing : Forged requests bypass authentication.
Replay Attack : Captured requests are resent later.
Man‑in‑the‑Middle (MITM) : Full interception and modification of traffic.
Algorithm Selection
A hybrid approach is used: RSA‑2048 (or ECC P‑256) for key exchange and digital signatures, and AES‑256‑GCM for bulk data encryption. Symmetric encryption provides speed, while asymmetric encryption solves key‑distribution problems.
Hybrid Encryption Flow
┌─────────────────────────────────────────────────────────────┐
│ Hybrid Encryption Flow │
├─────────────────────────────────────────────────────────────┤
│ 1. Client requests server RSA public key │
│ 2. Server returns RSA public key │
│ 3. Client generates random AES key │
│ 4. Client encrypts AES key with RSA public key │
│ 5. Server decrypts AES key with RSA private key │
│ 6. Client encrypts business data with AES‑GCM, adds signature│
│ 7. Client sends encrypted data + signature to server │
│ 8. Server decrypts with AES key, verifies signature, │
│ checks timestamp & nonce (replay protection) │
│ 9. Server returns encrypted response │
└─────────────────────────────────────────────────────────────┘Core Encryption Elements
Encryption Scheme Core Elements:
Data Encryption:
- Algorithm: AES‑256‑GCM (or AES‑128‑CBC)
- Mode: GCM (provides authenticated encryption)
- Key length: 256 bit
Key Exchange:
- Algorithm: RSA‑2048 (or ECC P‑256)
- Rotate keys every 24 hours
Signature Verification:
- Algorithm: RSA‑SHA256 (or ECDSA‑SHA256)
- Sign content: request parameters + timestamp + nonce + appSecret
Replay Protection:
- Timestamp window (e.g., 5 minutes)
- One‑time nonce stored in Redis with TTLSignature Generation & Verification
The service builds a canonical string by sorting parameters, appending timestamp, nonce, and a shared appSecret, then hashes it with SHA‑256 and signs the hash with the server’s RSA private key. Verification repeats the same steps using the client’s public key.
public String generateSignature(Map<String, String> params, long timestamp, String nonce) throws Exception {
String sortedParams = sortParams(params);
String signContent = String.format("%s×tamp=%d&nonce=%s&appSecret=%s",
sortedParams, timestamp, nonce, appSecret);
String hash = SHA256Util.hash(signContent);
PrivateKey privateKey = cryptoService.getServerPrivateKey();
byte[] signature = RSAUtil.sign(hash.getBytes(), privateKey);
return Base64.getEncoder().encodeToString(signature);
}
public boolean verifySignature(Map<String, String> params, long timestamp, String nonce,
String receivedSignature, PublicKey clientPublicKey) throws Exception {
String sortedParams = sortParams(params);
String signContent = String.format("%s×tamp=%d&nonce=%s&appSecret=%s",
sortedParams, timestamp, nonce, appSecret);
String hash = SHA256Util.hash(signContent);
byte[] signature = Base64.getDecoder().decode(receivedSignature);
return RSAUtil.verify(hash.getBytes(), signature, clientPublicKey);
}Replay‑Attack Defenses
Two mechanisms are combined:
Timestamp validation : request timestamp must be within a configurable window (default 300 seconds).
Nonce cache : a UUID nonce is stored in Redis with the same TTL; a second use of the same nonce is rejected.
public boolean validateTimestamp(long requestTimestamp) {
long current = System.currentTimeMillis() / 1000;
long diff = Math.abs(current - requestTimestamp);
return diff <= 300; // 5 minutes
}
public boolean validateNonce(String nonce, String clientId) {
String key = "api:nonce:" + clientId + ":" + nonce;
Boolean isNew = redisTemplate.opsForValue()
.setIfAbsent(key, "1", 300, TimeUnit.SECONDS);
return Boolean.TRUE.equals(isNew);
}Project Structure & Dependencies
api-security-demo/
├── src/main/java/com/example/apisecurity/
│ ├── config/ # SecurityConfig (not shown)
│ ├── controller/ApiController.java
│ ├── service/ # CryptoService, SignatureService, ReplayAttackService
│ ├── filter/ApiSecurityFilter.java
│ ├── model/ApiRequest.java, ApiResponse.java
│ ├── exception/SecurityException.java
│ └── util/ # AESUtil, RSAUtil, SHA256Util
├── src/main/resources/application.yml
└── pom.xmlKey Maven dependencies (Spring Boot 3.2.0, Spring Web, Validation, Redis, Lombok, Apache Commons Codec, Bouncy Castle, Hutool, Spring Boot Test).
Key Utility Classes
AES Encryption Utility
package com.example.apisecurity.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
/**
* AES‑256‑GCM utility providing authenticated encryption.
*/
@Slf4j
public class AESUtil {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int KEY_SIZE = 256;
private static final int GCM_IV_LENGTH = 12; // 96 bits
private static final int GCM_TAG_LENGTH = 128; // 128 bits
public static SecretKey generateKey() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
keyGen.init(KEY_SIZE, new SecureRandom());
return keyGen.generateKey();
}
public static SecretKey getKeyFromBytes(byte[] keyBytes) {
return new SecretKeySpec(keyBytes, 0, keyBytes.length, ALGORITHM);
}
public static byte[] encrypt(byte[] data, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ct = cipher.doFinal(data);
byte[] result = new byte[iv.length + ct.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(ct, 0, result, iv.length, ct.length);
return result;
}
public static byte[] decrypt(byte[] encryptedData, SecretKey key) throws Exception {
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(encryptedData, 0, iv, 0, iv.length);
byte[] ct = new byte[encryptedData.length - GCM_IV_LENGTH];
System.arraycopy(encryptedData, GCM_IV_LENGTH, ct, 0, ct.length);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(ct);
}
public static String encryptAndBase64(String data, SecretKey key) throws Exception {
byte[] encrypted = encrypt(data.getBytes(StandardCharsets.UTF_8), key);
return Base64.encodeBase64String(encrypted);
}
public static String decryptFromBase64(String base64Data, SecretKey key) throws Exception {
byte[] encrypted = Base64.decodeBase64(base64Data);
byte[] decrypted = decrypt(encrypted, key);
return new String(decrypted, StandardCharsets.UTF_8);
}
public static String keyToBase64(SecretKey key) {
return Base64.encodeBase64String(key.getEncoded());
}
public static SecretKey keyFromBase64(String base64Key) {
byte[] keyBytes = Base64.decodeBase64(base64Key);
return getKeyFromBytes(keyBytes);
}
}RSA Utility
package com.example.apisecurity.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/** RSA utility for key generation, encryption, decryption, signing and verification */
@Slf4j
public class RSAUtil {
private static final String ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final int KEY_SIZE = 2048;
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator gen = KeyPairGenerator.getInstance(ALGORITHM);
gen.initialize(KEY_SIZE);
return gen.generateKeyPair();
}
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
public static byte[] decrypt(byte[] encryptedData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptedData);
}
public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception {
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initSign(privateKey);
sig.update(data);
return sig.sign();
}
public static boolean verify(byte[] data, byte[] signature, PublicKey publicKey) throws Exception {
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(data);
return sig.verify(signature);
}
public static String publicKeyToBase64(PublicKey publicKey) {
return Base64.encodeBase64String(publicKey.getEncoded());
}
public static String privateKeyToBase64(PrivateKey privateKey) {
return Base64.encodeBase64String(privateKey.getEncoded());
}
public static PublicKey publicKeyFromBase64(String base64Key) throws Exception {
byte[] keyBytes = Base64.decodeBase64(base64Key);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory factory = KeyFactory.getInstance(ALGORITHM);
return factory.generatePublic(spec);
}
public static PrivateKey privateKeyFromBase64(String base64Key) throws Exception {
byte[] keyBytes = Base64.decodeBase64(base64Key);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory factory = KeyFactory.getInstance(ALGORITHM);
return factory.generatePrivate(spec);
}
public static String encryptAndBase64(String data, PublicKey publicKey) throws Exception {
byte[] encrypted = encrypt(data.getBytes(StandardCharsets.UTF_8), publicKey);
return Base64.encodeBase64String(encrypted);
}
public static String decryptFromBase64(String base64Data, PrivateKey privateKey) throws Exception {
byte[] encrypted = Base64.decodeBase64(base64Data);
byte[] decrypted = decrypt(encrypted, privateKey);
return new String(decrypted, StandardCharsets.UTF_8);
}
}SHA‑256 Utility
package com.example.apisecurity.util;
import org.apache.commons.codec.binary.Hex;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/** SHA‑256 hashing utility */
public class SHA256Util {
public static String hash(String data) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(data.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(hashBytes);
}
public static String hmac(String data, String key) throws Exception {
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
javax.crypto.spec.SecretKeySpec keySpec = new javax.crypto.spec.SecretKeySpec(
key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(hmacBytes);
}
}Core Service Implementations
CryptoService (key management, encryption/decryption)
package com.example.apisecurity.service;
import com.example.apisecurity.util.AESUtil;
import com.example.apisecurity.util.RSAUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.concurrent.TimeUnit;
/** Handles RSA key pair, session AES keys, and data encryption */
@Slf4j
@Service
@RequiredArgsConstructor
public class CryptoService {
private final StringRedisTemplate redisTemplate;
private final KeyPair serverKeyPair;
private static final String SERVER_PRIVATE_KEY = "server_private_key";
private static final String SERVER_PUBLIC_KEY = "server_public_key";
public void initServerKeyPair() throws Exception {
String storedPriv = redisTemplate.opsForValue().get(SERVER_PRIVATE_KEY);
String storedPub = redisTemplate.opsForValue().get(SERVER_PUBLIC_KEY);
if (storedPriv != null && storedPub != null) {
log.info("Loaded existing server key pair from Redis");
return;
}
KeyPair kp = RSAUtil.generateKeyPair();
redisTemplate.opsForValue().set(SERVER_PRIVATE_KEY, RSAUtil.privateKeyToBase64(kp.getPrivate()));
redisTemplate.opsForValue().set(SERVER_PUBLIC_KEY, RSAUtil.publicKeyToBase64(kp.getPublic()));
log.info("Server key pair initialized");
}
public String getServerPublicKey() {
return redisTemplate.opsForValue().get(SERVER_PUBLIC_KEY);
}
private PrivateKey getServerPrivateKey() throws Exception {
String privBase64 = redisTemplate.opsForValue().get(SERVER_PRIVATE_KEY);
return RSAUtil.privateKeyFromBase64(privBase64);
}
public SecretKey decryptAesKey(String encryptedAesKeyBase64, String clientId) throws Exception {
PrivateKey priv = getServerPrivateKey();
String decrypted = RSAUtil.decryptFromBase64(encryptedAesKeyBase64, priv);
return AESUtil.keyFromBase64(decrypted);
}
public String decryptBusinessData(String encryptedData, SecretKey aesKey) throws Exception {
return AESUtil.decryptFromBase64(encryptedData, aesKey);
}
public String encryptBusinessData(String data, SecretKey aesKey) throws Exception {
return AESUtil.encryptAndBase64(data, aesKey);
}
public void cacheSessionKey(String clientId, SecretKey aesKey) {
String key = "api:session:" + clientId;
redisTemplate.opsForValue().set(key, AESUtil.keyToBase64(aesKey), 24, TimeUnit.HOURS);
log.info("Cached session key for clientId {}", clientId);
}
public SecretKey getSessionKey(String clientId) {
String key = "api:session:" + clientId;
String base64 = redisTemplate.opsForValue().get(key);
return base64 == null ? null : AESUtil.keyFromBase64(base64);
}
}SignatureService (signature generation & verification)
package com.example.apisecurity.service;
import com.example.apisecurity.util.RSAUtil;
import com.example.apisecurity.util.SHA256Util;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
/** Generates and verifies RSA‑SHA256 signatures */
@Slf4j
@Service
@RequiredArgsConstructor
public class SignatureService {
private final CryptoService cryptoService;
@Value("${api.security.app-secret:defaultSecret}")
private String appSecret;
private String sortParams(Map<String, String> params) {
return new TreeMap<>(params).entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
}
public String generateSignature(Map<String, String> params, long timestamp, String nonce) throws Exception {
String sorted = sortParams(params);
String signContent = String.format("%s×tamp=%d&nonce=%s&appSecret=%s",
sorted, timestamp, nonce, appSecret);
log.debug("Signature raw content: {}", signContent);
String hash = SHA256Util.hash(signContent);
PrivateKey priv = cryptoService.getServerPrivateKey();
byte[] sig = RSAUtil.sign(hash.getBytes(), priv);
return java.util.Base64.getEncoder().encodeToString(sig);
}
public boolean verifySignature(Map<String, String> params, long timestamp, String nonce,
String receivedSignature, PublicKey clientPublicKey) throws Exception {
String sorted = sortParams(params);
String signContent = String.format("%s×tamp=%d&nonce=%s&appSecret=%s",
sorted, timestamp, nonce, appSecret);
String hash = SHA256Util.hash(signContent);
byte[] sig = java.util.Base64.getDecoder().decode(receivedSignature);
return RSAUtil.verify(hash.getBytes(), sig, clientPublicKey);
}
}ReplayAttackService (timestamp & nonce validation)
package com.example.apisecurity.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/** Prevents replay attacks using timestamp window and one‑time nonce */
@Slf4j
@Service
@RequiredArgsConstructor
public class ReplayAttackService {
private final StringRedisTemplate redisTemplate;
private static final long MAX_TIME_DIFF = 300; // seconds (5 minutes)
public boolean validateTimestamp(long requestTimestamp) {
long current = System.currentTimeMillis() / 1000;
long diff = Math.abs(current - requestTimestamp);
if (diff > MAX_TIME_DIFF) {
log.warn("Timestamp validation failed: diff {}s", diff);
return false;
}
return true;
}
public boolean validateNonce(String nonce, String clientId) {
String key = "api:nonce:" + clientId + ":" + nonce;
Boolean isNew = redisTemplate.opsForValue()
.setIfAbsent(key, "1", MAX_TIME_DIFF, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(isNew)) {
log.warn("Nonce reuse detected for clientId {} nonce {}", clientId, nonce);
return false;
}
return true;
}
public boolean validateRequest(long timestamp, String nonce, String clientId) {
return validateTimestamp(timestamp) && validateNonce(nonce, clientId);
}
}Security Filter
package com.example.apisecurity.filter;
import com.example.apisecurity.exception.SecurityException;
import com.example.apisecurity.model.ApiRequest;
import com.example.apisecurity.model.ApiResponse;
import com.example.apisecurity.service.CryptoService;
import com.example.apisecurity.service.ReplayAttackService;
import com.example.apisecurity.service.SignatureService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.util.Map;
/** Filters incoming API requests: decrypts payload, validates replay protection, and verifies signatures */
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiSecurityFilter extends OncePerRequestFilter {
private final ObjectMapper objectMapper;
private final CryptoService cryptoService;
private final SignatureService signatureService;
private final ReplayAttackService replayAttackService;
private static final String[] EXCLUDED_PATHS = {"/api/public/**", "/api/key/exchange", "/actuator/**"};
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String uri = request.getRequestURI();
if (isExcluded(uri)) { chain.doFilter(request, response); return; }
try {
String body = getRequestBody(request);
ApiRequest apiReq = objectMapper.readValue(body, ApiRequest.class);
if (!replayAttackService.validateRequest(apiReq.getTimestamp(), apiReq.getNonce(), apiReq.getClientId())) {
throw new SecurityException("Possible replay attack");
}
SecretKey sessionKey = cryptoService.getSessionKey(apiReq.getClientId());
if (sessionKey == null) throw new SecurityException("Session key missing");
String decrypted = cryptoService.decryptBusinessData(apiReq.getEncryptedData(), sessionKey);
Map<String, String> params = objectMapper.readValue(decrypted, Map.class);
boolean ok = signatureService.verifySignature(params, apiReq.getTimestamp(), apiReq.getNonce(),
apiReq.getSignature(), null); // client public key should be provided in real scenario
if (!ok) throw new SecurityException("Signature verification failed");
request.setAttribute("DECRYPTED_DATA", decrypted);
request.setAttribute("CLIENT_ID", apiReq.getClientId());
log.info("Security check passed for clientId {}", apiReq.getClientId());
chain.doFilter(request, response);
} catch (SecurityException se) {
log.error("Security check failed: {}", se.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.error(401, se.getMessage())));
} catch (Exception e) {
log.error("Request processing error", e);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.error(400, "Request processing failed")));
}
}
private boolean isExcluded(String requestURI) {
for (String pattern : EXCLUDED_PATHS) {
if (requestURI.matches(pattern.replace("**", ".*"))) return true;
}
return false;
}
private String getRequestBody(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
try (java.io.BufferedReader reader = request.getReader()) {
while ((line = reader.readLine()) != null) sb.append(line);
}
return sb.toString();
}
}Controller Endpoints
package com.example.apisecurity.controller;
import com.example.apisecurity.model.ApiRequest;
import com.example.apisecurity.model.ApiResponse;
import com.example.apisecurity.service.CryptoService;
import com.example.apisecurity.service.SignatureService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.crypto.SecretKey;
import java.util.Map;
/** API controller exposing key exchange and a protected business endpoint */
@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ApiController {
private final ObjectMapper objectMapper;
private final CryptoService cryptoService;
private final SignatureService signatureService;
@PostMapping("/key/exchange")
public ApiResponse<Map<String, String>> keyExchange(@RequestBody ApiRequest req) {
try {
String serverPub = cryptoService.getServerPublicKey();
SecretKey aes = cryptoService.decryptAesKey(req.getEncryptedAesKey(), req.getClientId());
cryptoService.cacheSessionKey(req.getClientId(), aes);
Map<String, String> result = Map.of("serverPublicKey", serverPub, "status", "success");
log.info("Key exchange successful for clientId {}", req.getClientId());
return ApiResponse.success(result);
} catch (Exception e) {
log.error("Key exchange failed", e);
return ApiResponse.error(500, "Key exchange failed: " + e.getMessage());
}
}
@GetMapping("/public/key")
public ApiResponse<String> getPublicKey() {
return ApiResponse.success(cryptoService.getServerPublicKey());
}
@PostMapping("/order/create")
public ApiResponse<Map<String, Object>> createOrder(HttpServletRequest request) {
try {
String data = (String) request.getAttribute("DECRYPTED_DATA");
String clientId = (String) request.getAttribute("CLIENT_ID");
Map<String, Object> order = objectMapper.readValue(data, Map.class);
// Business logic placeholder
Map<String, Object> result = Map.of(
"orderId", "ORD" + System.currentTimeMillis(),
"status", "created"
);
log.info("Order created for clientId {}: {}", clientId, result);
return ApiResponse.success(result);
} catch (Exception e) {
log.error("Create order failed", e);
return ApiResponse.error(500, "Create order failed");
}
}
}Configuration (application.yml)
server:
port: 8080
spring:
application:
name: api-security-demo
data:
redis:
host: localhost
port: 6379
database: 0
timeout: 5000ms
api:
security:
app-secret: ${API_APP_SECRET:your-256-bit-secret-key-here}
key-rotate-hours: 24
max-time-diff: 300
logging:
level:
com.example.apisecurity: DEBUGEnterprise Best Practices
Key Management
Never hard‑code keys; store them in a KMS (AWS KMS, Aliyun KMS) or HSM.
Rotate keys regularly (recommended 24‑72 hours) with versioning and a grace period.
Apply least‑privilege IAM policies and audit key usage.
Transport Security
Enforce HTTPS with TLS 1.3; disable weak cipher suites.
Use trusted CA‑issued certificates and automate renewal.
Enable HSTS to prevent protocol downgrade attacks.
Rate Limiting & Circuit Breaking
Example using Resilience4j to limit API calls to 100 req/s with a 500 ms timeout.
RateLimiter<Request> apiRateLimiter = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(100)
.timeoutDuration(Duration.ofMillis(500))
.build();Audit Logging
Log request time, source IP, client ID, sanitized parameters, response status, and processing latency.
Record security events (signature failures, replay attempts) separately.
Centralize logs with ELK/EFK; retain at least 180 days, keep critical security logs indefinitely.
Security Testing
Regular third‑party penetration tests covering replay, tampering, and MITM scenarios.
Automated unit and integration tests for encryption, decryption, and signature verification.
Static analysis (SonarQube) and dynamic scanning (SAST/DAST) for code‑level vulnerabilities.
Design Principles
Depth‑in‑Defense : Multiple independent layers protect the system.
Least Privilege : Keys and permissions are granted only as needed.
Secure‑by‑Default : Security features are enabled out‑of‑the‑box.
Auditability : Every security‑relevant action is logged and traceable.
Continuous Evolution : Regular reviews and updates of algorithms, key lifecycles, and policies.
References
OWASP API Security Top 10 – https://owasp.org/www-project-api-security/
NIST Cryptographic Standards – https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines
Spring Security Documentation – https://spring.io/projects/spring-security
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.
Cloud Architecture
Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.
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.
