Information Security 29 min read

Implementing Symmetric & Asymmetric Encryption, Digital Signatures, and URL Protection with Spring Cloud Gateway

This article explains symmetric and asymmetric encryption principles, digital signatures, HTTPS with certificate authorities, and demonstrates how to implement combined encryption, digital signing, and URL protection using Spring Cloud Gateway filters and Java code examples.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing Symmetric & Asymmetric Encryption, Digital Signatures, and URL Protection with Spring Cloud Gateway

Symmetric Encryption

Symmetric encryption uses the same secret key for both encryption and decryption. The basic workflow consists of four steps:

Key generation: Both parties agree on a shared secret key, or one party generates the key and securely transmits it to the other.

Encryption: The sender encrypts the original data with the shared key, producing ciphertext.

Transmission: The ciphertext is sent to the receiver.

Decryption: The receiver uses the same shared key to decrypt the ciphertext back to the original data.

Asymmetric Encryption

Asymmetric encryption uses a pair of keys – a public key and a private key – for encryption and decryption. Its typical process includes:

Key‑pair generation: A public key and a private key are created; the public key can be shared openly while the private key must be kept secret.

Public‑key distribution: The public key is sent to the party that needs to encrypt data.

Encryption: The sender encrypts the plaintext with the receiver’s public key, producing ciphertext.

Transmission: The ciphertext is transmitted to the receiver.

Decryption: The receiver uses its private key to decrypt the ciphertext back to the original data.

In real‑world applications, symmetric and asymmetric encryption are often combined: asymmetric encryption securely exchanges a symmetric key, and the symmetric key is then used for fast data encryption/decryption.

What Is a Digital Signature?

A digital signature provides integrity and non‑repudiation. After two parties have exchanged public keys, the sender (A) hashes the original data, encrypts the hash with its private key, and appends the result to the message. The receiver (B) decrypts the signature with A’s public key, hashes the received plaintext, and compares the two hashes. If they match, the data has not been altered; otherwise, tampering is detected.

HTTPS and Certificate Authorities (CA)

HTTPS is simply HTTP over TLS/SSL, which uses a combination of asymmetric (RSA) and symmetric (AES) cryptography. The CA plays a crucial role in establishing trust:

Certificate issuance: The CA signs a digital certificate containing the server’s public key and identity information.

Secure connection establishment: When a client first connects, the server presents its certificate. The client verifies the CA’s signature, the certificate’s validity period, and other attributes. Successful verification assures the client it is communicating with the legitimate server.

Key exchange: The client generates a random symmetric session key, encrypts it with the server’s public key, and sends it to the server. The server decrypts it with its private key, obtaining the session key.

Data encryption and transmission: Both sides use the session key to encrypt and decrypt all subsequent traffic, ensuring confidentiality even if the data is intercepted.

Integrity check: TLS adds a Message Authentication Code (MAC) to each record, guaranteeing that the data has not been altered in transit.

In TLS, the first two random numbers are exchanged in clear text; the pre‑master secret is obtained using RSA. The pre‑master secret then encrypts the first and second random numbers to derive the session key. The session key is used only for the current session, and a new key is generated for each new session, greatly enhancing security. RSA is used only for the pre‑master secret exchange because asymmetric encryption is computationally expensive; the rest of the communication uses fast AES symmetric encryption.

Gateway Filter Chain

Spring Cloud Gateway allows custom global filters that implement the Ordered interface to control execution order. A typical filter method looks like:

public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain)

The exchange object contains the request and response, while chain represents the remaining filters.

Filters can modify the request and response, for example by changing the 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();

After modifying the URI, the request is routed to the new path by the ForwardRoutingFilter and eventually reaches the target handler.

How to Add a Digital Signature to Your Path

The workflow combines RSA for exchanging a symmetric AES key and then uses AES for fast data encryption and signing:

公钥获取:
客户端首先通过一个特定的接口从服务器获取RSA公钥。

对称密钥加密:
客户端生成一个随机的对称密钥,然后使用服务器的RSA公钥对这个对称密钥进行加密。

发送加密的对称密钥:
客户端将加密后的对称密钥发送到服务器。

对称密钥解密:
服务器使用自己的RSA私钥解密客户端发送的加密对称密钥,得到原始的对称密钥。

加密通信:
之后,客户端和服务器都使用该对称密钥进行AES加密/解密,包括URL动态加密、请求/响应加密以及数字签名验证。

数字签名:
使用对称密钥对请求参数生成签名,接收方使用相同密钥验证签名,以确保数据完整性和不可否认性。

Frontend Obtains RSA Public Key

A Spring configuration class generates an RSA key pair at startup and exposes an endpoint /public-key that returns the Base64‑encoded public key.

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 公众号:码猿技术专栏
 * @date 2023/10/2 15:13
 * SecurityConfig returns the RSA public key.
 */
@Configuration
public class SecurityConfig {
    private KeyPair keyPair;

    @PostConstruct
    public void init() {
        // Generate RSA key pair
        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);
        }
    }

    /** Provide public key to frontend */
    @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;
    }
}

Frontend Encrypts the Symmetric Key

The client encrypts the symmetric key with the obtained RSA public key:

package blossom.star.project.product;

import org.junit.jupiter.api.Test;
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

class RSA {
    @Test
    void contextLoads() {
        // placeholder test
    }

    public static void main(String[] args) throws Exception {
        // Replace with your RSA public key (Base64 encoded)
        String publicKeyPEM = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXBSqSyOPb01/...";
        // Replace with your symmetric key
        String symmetricKey = "zhangjinbiao6666";

        // Convert PEM to PublicKey
        byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded);
        PublicKey publicKey = keyFactory.generatePublic(keySpec);

        // Encrypt symmetric key
        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 Receives and Stores the Symmetric Key

A global filter extracts the X-Encrypted-Symmetric-Key header, decrypts it with the RSA private key, and stores the resulting AES key in Redis (or any other store) for the current session.

package blossom.star.project.gateway.filter;

import blossom.star.framework.common.constant.HttpStatus;
import blossom.star.project.gateway.config.SecurityConfig;
import blossom.star.project.gateway.util.GatewayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.Cipher;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * Symmetric key storage filter.
 * Retrieves the encrypted symmetric key from request headers, decrypts it, and saves it to Redis.
 */
//@Component
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);
                // Store in Redis (key could be session‑id based)
                String redisKey = "symmetric:key:1"; // placeholder
                stringRedisTemplate.opsForValue().set(redisKey, symmetricKey);
            } catch (Exception e) {
                e.printStackTrace();
                String responseBody = "Error decrypting symmetric key!";
                GatewayUtil.responseMessage(exchange, responseBody);
            }
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -300;
    }
}

Frontend Sends AES‑Encrypted Request

The client encrypts request parameters (e.g., productId=1 ) with the shared AES key and appends a signature generated from the same key. The encrypted payload is placed after the /encrypt/ path segment.

public class AES {
    // Encrypt a URL parameter string using AES and return Base64 text
    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"; // 16‑byte key
        String encryptedText = encryptUrl(plaintext, symmetricKey);
        System.out.println(encryptedText);
    }
}

Example request:

http://localhost:8080/v1/product/encrypt/WyYSV30Cor8QX/eWGsQ7yPD3EvNRRS0HF845UOb+KAdwHPKZByMa3250J/z2S4at

Validate the Request

A second global filter decrypts the URL segment, extracts query parameters, recomputes the signature using the stored AES key, and compares it with the provided signature . If they differ, an error response is returned.

package blossom.star.project.gateway.filter;

import blossom.star.project.gateway.util.CryptoHelper;
import blossom.star.project.gateway.util.GatewayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.net.URISyntaxException;

@Component
public class CryptoFilter implements GlobalFilter, Ordered {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private CryptoHelper cryptoHelper;

    @Override
    public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String redisKey = "symmetric:key:1"; // placeholder
        String symmetricKey = stringRedisTemplate.opsForValue().get(redisKey);
        if (symmetricKey == null) {
            return GatewayUtil.responseMessage(exchange, "this session has no 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);
            // Replace request URI with decrypted one
            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();
            String signature = uriComponents.getQueryParams().getFirst("signature");
            if (!cryptoHelper.verifySignature(uriComponents.getQueryParams(), 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;
    }
}

CryptoHelper Utility

package blossom.star.project.gateway.util;

import org.springframework.context.annotation.Configuration;
import org.springframework.util.MultiValueMap;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Map;

/**
 * Cryptographic helper for AES encryption/decryption and signature verification.
 */
@Configuration
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); // remove trailing '&'
        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 gateway successfully decrypts the URL, validates the signature, and forwards the request to the intended service.

Dynamic URL Encryption

The actual request URL contains an /encrypt/ segment followed by the Base64‑encoded AES ciphertext. The gateway removes the /encrypt/ marker, decrypts the remaining part, and reconstructs the original query string before routing.

http://localhost:8080/v1/product/encrypt/WLB8EDs2LNTsUJpS/aANt0XqZ4MvgpFqN/SwDBVwDbERjBkQw62kfAmfsDW2Bngm

Thus, the encryption logic is transparent to downstream services.

Conclusion

The article demonstrates how to combine RSA and AES to achieve secure key exchange, request signing, and dynamic URL encryption within a Spring Cloud Gateway environment. It also provides complete code snippets for key generation, public‑key exposure, symmetric‑key storage, request encryption, and signature verification.

If you found this tutorial helpful, please like, view, share, or bookmark the post. Your support encourages the author to continue sharing technical knowledge.

For more in‑depth projects and source‑code walkthroughs, consider joining the author's knowledge community (price: 199 CNY) where additional resources such as Spring‑full‑stack projects, massive‑data sharding, DDD micro‑services, and RocketMQ deep dives are available.

RSAEncryptioninformation securityTLSSpring Cloud Gatewaydigital signatureAES
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.