Implementing Symmetric and Asymmetric Encryption, Digital Signatures, and Dynamic URL Encryption in Spring Cloud Gateway
This article explains the principles of symmetric and asymmetric encryption, digital signatures, HTTPS with CA, and demonstrates how to generate RSA keys, handle symmetric keys, encrypt URLs with AES, and verify signatures using custom Spring Cloud Gateway filters, complete with Java code examples.
Hello everyone, I am Chen.
When transmitting data over a network, encryption is used to prevent tampering, typically using symmetric (e.g., AES) or asymmetric (e.g., RSA) algorithms.
Symmetric Encryption
Symmetric encryption uses the same key for encryption and decryption. The basic steps are key generation, encryption, transmission, and decryption.
Key generation: Both parties agree on a shared key or one party generates it and securely transmits it.
Encryption: The shared key encrypts the original data.
Transmission: The encrypted data is sent to the other party.
Decryption: The receiver uses the same shared key to decrypt the data.
Asymmetric Encryption
Asymmetric encryption uses a public key for encryption and a private key for decryption.
Key pair generation: Generate a public/private key pair.
Public key distribution: Send the public key to the party that needs to encrypt data.
Encryption: Encrypt data with the public key.
Transmission: Send the encrypted data.
Decryption: The receiver decrypts with the private key.
In practice, symmetric and asymmetric encryption are combined: asymmetric encryption exchanges the symmetric key, then symmetric encryption secures the data.
What Is a Digital Signature?
After exchanging public keys, Party A encrypts the data with B's public key, then appends a hash of the original data encrypted with A's private key. B decrypts with its private key, verifies the hash using A's public key, ensuring integrity and authenticity.
HTTPS and CA
HTTPS uses TLS/SSL, which combines asymmetric (RSA) and symmetric (AES) encryption. A Certificate Authority (CA) issues digital certificates containing the server's public key, enabling clients to verify server identity and establish a secure session key.
Certificate issuance: CA signs the server's certificate.
Secure connection establishment: Client validates the certificate and confirms the server's identity.
Key exchange: Client encrypts a random symmetric key with the server's public key and sends it.
Data encryption and transmission: Both sides use the symmetric session key to encrypt traffic.
Integrity check: TLS adds a MAC to ensure data is not altered.
Gateway Filter Chain
Custom global filters can be ordered by implementing the Ordered interface. Below are three example filters.
The filter method signature is:
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain)The exchange provides request/response objects, and chain represents the filter chain.
Example code to modify the request URI:
exchange = exchange.mutate().request(build -> {
try {
build.uri(new URI("http://localhost:8080/v1/product?productId=1"))
.build();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}).build();How to Set a Digital Signature for Your Path
The workflow uses RSA to exchange a symmetric key, then AES for request encryption and signature generation.
RSA public key endpoint:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import javax.annotation.PostConstruct;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* @author: Chen
* @date: 2023/10/2
* Returns RSA public key
*/
@Configuration
public class SecurityConfig {
private KeyPair keyPair;
@PostConstruct
public void init() {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
keyPair = keyGen.genKeyPair();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Failed to generate RSA key pair", e);
}
}
@Bean
public RouterFunction
publicKeyEndpoint() {
return RouterFunctions.route()
.GET("/public-key", req -> {
String publicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
return ServerResponse.ok().bodyValue(publicKey);
})
.build();
}
public KeyPair getKeyPair() {
return keyPair;
}
}Client encrypts symmetric key with RSA:
import java.util.Base64;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
public class RSA {
public static void main(String[] args) throws Exception {
String publicKeyPEM = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXBSqSyOPb01/...";
String symmetricKey = "zhangjinbiao6666";
byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded);
PublicKey publicKey = keyFactory.generatePublic(keySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedSymmetricKey = cipher.doFinal(symmetricKey.getBytes());
String encryptedSymmetricKeyBase64 = Base64.getEncoder().encodeToString(encryptedSymmetricKey);
System.out.println(encryptedSymmetricKeyBase64);
}
}Backend filter to store the symmetric key in Redis:
public class SymmetricKeyFilter implements GlobalFilter, Ordered {
@Autowired
private SecurityConfig securityConfig;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String encryptedSymmetricKey = exchange.getRequest().getHeaders().getFirst("X-Encrypted-Symmetric-Key");
if (encryptedSymmetricKey != null) {
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, securityConfig.getKeyPair().getPrivate());
byte[] decryptedKeyBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedSymmetricKey));
String symmetricKey = new String(decryptedKeyBytes, StandardCharsets.UTF_8);
String redisSymmetricKey = "symmetric:key:" + 1;
stringRedisTemplate.opsForValue().set(redisSymmetricKey, symmetricKey);
} catch (Exception e) {
e.printStackTrace();
GatewayUtil.responseMessage(exchange, "there are something wrong occurs when decrypt your key!!!");
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() { return -300; }
}Encrypting a URL with AES:
public class AES {
public static String encryptUrl(String url, String symmetricKey) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encryptedBytes = cipher.doFinal(url.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
public static void main(String[] args) throws Exception {
String plaintext = "productId=1";
String symmetricKey = "zhangjinbiao6666";
System.out.println(encryptUrl(plaintext, symmetricKey));
}
}Filter that decrypts the URL, verifies the signature, and rewrites the request:
public class CryptoFilter implements GlobalFilter, Ordered {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private CryptoHelper cryptoHelper;
@Override
public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String redisSymmetricKey = "symmetric:key:" + 1;
String symmetricKey = stringRedisTemplate.opsForValue().get(redisSymmetricKey);
if (symmetricKey == null) {
return GatewayUtil.responseMessage(exchange, "this session has not symmetricKey!!!");
}
try {
String encryptedUrl = exchange.getRequest().getURI().toString();
String path = exchange.getRequest().getURI().getPath();
String encryptPathParam = path.substring(path.indexOf("/encrypt/") + 9);
String decryptedPathParam = cryptoHelper.decryptUrl(encryptPathParam, symmetricKey);
String decryptedUri = encryptedUrl.substring(0, encryptedUrl.indexOf("/encrypt/"))
.concat("?").concat(decryptedPathParam);
exchange = exchange.mutate().request(build -> {
try {
build.uri(new URI(decryptedUri));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}).build();
UriComponents uriComponents = UriComponentsBuilder.fromUriString(decryptedUri).build();
MultiValueMap
decryptedQueryParams = uriComponents.getQueryParams();
String signature = decryptedQueryParams.getFirst("signature");
if (!cryptoHelper.verifySignature(decryptedQueryParams, signature, symmetricKey)) {
return GatewayUtil.responseMessage(exchange, "the param has something wrong!!!");
}
} catch (Exception e) {
return GatewayUtil.responseMessage(exchange, "the internal server occurs an error!!!");
}
return chain.filter(exchange);
}
@Override
public int getOrder() { return -200; }
}Utility class for decryption and signature verification:
public class CryptoHelper {
public String decryptUrl(String encryptedUrl, String symmetricKey) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedUrl));
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
public boolean verifySignature(MultiValueMap
queryParams, String signature, String symmetricKey) throws Exception {
StringBuilder sb = new StringBuilder();
for (Map.Entry
> entry : queryParams.entrySet()) {
if (!"signature".equals(entry.getKey())) {
sb.append(entry.getKey()).append("=").append(String.join(",", entry.getValue())).append("&");
}
}
sb.setLength(sb.length() - 1);
String computedSignature = encryptRequestParam(sb.toString(), symmetricKey);
return computedSignature.equals(signature);
}
public static String encryptRequestParam(String requestParam, String symmetricKey) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encryptedBytes = cipher.doFinal(requestParam.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
}When the request parameters are unchanged, the system correctly processes the request; otherwise, it returns an error.
How to Implement Dynamic URL Encryption?
The encrypted part after /encrypt/ contains the AES‑encrypted query string and signature. The gateway filter decrypts it, verifies the signature, and forwards the request to the appropriate handler.
Final Note (Please Support)
If you found this article helpful, please like, view, share, and bookmark it. Your support motivates me to keep sharing.
For more advanced content, consider joining my Knowledge Planet for 199 CNY, which includes projects on Spring, micro‑services, massive data sharding, DDD, and more.
To join, add my WeChat: special_coder
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.