Information Security 22 min read

Implementing RSA Encryption, Timestamp Validation, and Signature Verification in Spring Cloud Gateway

This article provides a comprehensive guide on securing Spring Cloud Gateway APIs using RSA asymmetric encryption, timestamp-based replay protection, unique request IDs stored in Redis, and MD5 signature verification, complete with detailed Java and front‑end code examples and testing results.

Architect
Architect
Architect
Implementing RSA Encryption, Timestamp Validation, and Signature Verification in Spring Cloud Gateway

The article begins by describing the risk of data interception during user login, where attackers can capture form data and obtain credentials.

RSA Overview

RSA is an asymmetric encryption algorithm invented in 1977 by Rivest, Shamir, and Adleman. Its security relies on the difficulty of factoring large integers; short keys can be broken, but sufficiently long keys remain secure.

RSA Application Process

Generate a public/private key pair; the public key is shared, the private key is kept secret.

Encrypt data with the public key on the client side.

Decrypt data with the private key on the server side.

RSA Utility Class (Java)

package com.demo.utils;

import java.util.Map;
import lombok.extern.slf4j.Slf4j;

public class RSAUtils {
    public static final String PUBLIC_KEY = "public_key";
    public static final String PRIVATE_KEY = "private_key";

    public static final Map
generateRasKey() { /* key generation logic */ }
    public static String encrypt(String str, String publicKey) { /* encryption logic */ }
    public static String decrypt(String str, String privateKey) { /* decryption logic */ }
}

class RsaException extends RuntimeException {
    private final String message;
    public RsaException(String message) { this.message = message; }
}

Unit tests demonstrate key generation, encryption, and decryption using the above utilities.

Front‑End Login Page (HTML & JavaScript)

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>登录页面</title></head>
<body>
  <h1>登录</h1>
  <form id="from">
    账号:<input id="username" type="text"/><br/>
    密码:<input id="password" type="password"/><br/>
    <input id="btn_login" type="button" value="登录"/>
  </form>
  <script src="js/jquery.min.js"></script>
  <script src="js/jsencrypt.js"></script>
  <script src="js/md5.min.js"></script>
  <script type="text/javascript">
    var encrypt = new JSEncrypt();
    encrypt.setPublicKey("MIGfMA0GCSqGSIb3DQEBA...AQAB");
    document.getElementById('btn_login').onclick = function() {
      var username = $('#username').val();
      var password = $('#password').val();
      var form = {username: username, password: password};
      var timestamp = Date.parse(new Date());
      var requestId = getUuid();
      var data = JSON.stringify(sort_ASCII(form));
      var sign = MD5(data + requestId + timestamp);
      $.ajax({
        url: "http://localhost:9000/api/user/login",
        beforeSend: function(xhr) {
          xhr.setRequestHeader("timestamp", timestamp);
          xhr.setRequestHeader("requestId", requestId);
          xhr.setRequestHeader("sign", sign);
        },
        data: encrypt.encrypt(data),
        type: "POST",
        contentType: "application/json;charset=utf-8",
        success: function(data){ console.log(data); }
      });
    };
    // getUuid and sort_ASCII functions omitted for brevity
  </script>
</body>
</html>

The front‑end encrypts the JSON payload with the RSA public key, adds a timestamp, a UUID (requestId), and an MD5 signature before sending the request.

Gateway Filter Configuration (Java)

package com.demo.gateway.config;

import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
public class GatewayFilterConfig implements GlobalFilter, Ordered {
    @Autowired
    private RedisTemplate
redisTemplate;
    private static final String ERROR_MESSAGE = "拒绝服务";

    @Override
    public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Long dateTimestamp = getDateTimestamp(exchange.getRequest().getHeaders());
        String requestId = getRequestId(exchange.getRequest().getHeaders());
        String sign = getSign(exchange.getRequest().getHeaders());
        String requestUrl = exchange.getRequest().getPath().value();
        if (!new AntPathMatcher().match("/user/login", requestUrl)) {
            String token = exchange.getRequest().getHeaders().getFirst(UserConstant.TOKEN);
            Claims claim = TokenUtils.getClaim(token);
            if (StringUtils.isBlank(token) || claim == null) {
                return FilterUtils.invalidToken(exchange);
            }
        }
        Map
paramMap = updateRequestParam(exchange);
        ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
        Mono
modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
            String decrypt = RSAUtils.decrypt(body, RSAConstant.PRIVATE_KEY);
            JSONObject json = JSON.parseObject(decrypt);
            json.forEach((k,v) -> paramMap.put(k, v));
            checkSign(sign, dateTimestamp, requestId, paramMap);
            return Mono.just(decrypt);
        });
        BodyInserter
, ReactiveHttpOutputMessage> inserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        MyCachedBodyOutputMessage outputMessage = new MyCachedBodyOutputMessage(exchange, headers);
        outputMessage.initial(paramMap, requestId, sign, dateTimestamp);
        return inserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
            ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                @Override
                public Flux
getBody() {
                    Flux
body = outputMessage.getBody();
                    if (body.equals(Flux.empty())) {
                        checkSign(outputMessage.getSign(), outputMessage.getDateTimestamp(), outputMessage.getRequestId(), outputMessage.getParamMap());
                    }
                    return outputMessage.getBody();
                }
            };
            return chain.filter(exchange.mutate().request(decorator).build());
        }));
    }

    public void checkSign(String sign, Long dateTimestamp, String requestId, Map
paramMap) {
        String str = JSON.toJSONString(paramMap) + requestId + dateTimestamp;
        String expected = Md5Utils.getMD5(str.getBytes());
        if (!expected.equals(sign)) {
            throw new IllegalArgumentException(ERROR_MESSAGE);
        }
    }

    private Map
updateRequestParam(ServerWebExchange exchange) throws NoSuchFieldException, IllegalAccessException {
        ServerHttpRequest request = exchange.getRequest();
        URI uri = request.getURI();
        String query = uri.getQuery();
        if (StringUtils.isNotBlank(query) && query.contains("param")) {
            String[] split = query.split("=");
            String param = RSAUtils.decrypt(split[1], RSAConstant.PRIVATE_KEY);
            Field target = uri.getClass().getDeclaredField("query");
            target.setAccessible(true);
            target.set(uri, param);
            return getParamMap(param);
        }
        return new TreeMap<>();
    }

    private Map
getParamMap(String param) {
        Map
map = new TreeMap<>();
        for (String s : param.split("&")) {
            String[] kv = s.split("=");
            map.put(kv[0], kv[1]);
        }
        return map;
    }

    private String getSign(HttpHeaders headers) {
        List
list = headers.get("sign");
        if (CollectionUtils.isEmpty(list)) throw new IllegalArgumentException(ERROR_MESSAGE);
        return list.get(0);
    }

    private Long getDateTimestamp(HttpHeaders headers) {
        List
list = headers.get("timestamp");
        if (CollectionUtils.isEmpty(list)) throw new IllegalArgumentException(ERROR_MESSAGE);
        long ts = Long.parseLong(list.get(0));
        if (System.currentTimeMillis() - ts > 1000L * 60 * 5) throw new IllegalArgumentException(ERROR_MESSAGE);
        return ts;
    }

    private String getRequestId(HttpHeaders headers) {
        List
list = headers.get("requestId");
        if (CollectionUtils.isEmpty(list)) throw new IllegalArgumentException(ERROR_MESSAGE);
        String requestId = list.get(0);
        if (StringUtils.isNotBlank(redisTemplate.opsForValue().get(requestId))) {
            throw new IllegalArgumentException(ERROR_MESSAGE);
        }
        redisTemplate.opsForValue().set(requestId, requestId, 5, TimeUnit.MINUTES);
        return requestId;
    }

    @Override
    public int getOrder() { return 80; }
}

The filter extracts the timestamp, requestId, and signature from request headers, validates the timestamp (5‑minute window), ensures requestId uniqueness via Redis, decrypts the RSA‑encrypted payload, merges it into a parameter map, and finally verifies the MD5 signature before forwarding the request.

Additional helper classes such as MyCachedBodyOutputMessage store the extracted parameters for later verification.

Testing shows successful login and query operations with proper signature verification, and failure cases (e.g., missing timestamp) return appropriate error messages.

Repository address: https://gitee.com/zhurongsheng/springcloud-gateway-rsa

JavaMicroservicessecurityRSASpringBootencryptionSpring Cloud Gateway
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.