Secure Spring Cloud Gateway with RSA Encryption, Timestamp, UUID and Signature Verification

This article explains how to protect Spring Cloud Gateway APIs from data interception by using RSA asymmetric encryption, adding request timestamps and UUIDs for replay protection, and implementing request signatures, with detailed Java code examples and gateway filter configuration.

Programmer DD
Programmer DD
Programmer DD
Secure Spring Cloud Gateway with RSA Encryption, Timestamp, UUID and Signature Verification

1 防止数据抓包窃取

当用户登录时,攻击者可以通过抓包工具获取表单信息,从而窃取账号密码。

1.2 RSA 非对称加密

1.2.1 RSA简介

RSA 是一种非对称加密算法,由 Ron Rivest、Adi Shamir、Leonard Adleman 于 1977 年提出。

1.2.2 RSA应用过程

接收方生成公钥和私钥,公钥公开,私钥保留。

发送方使用公钥加密数据,接收方使用私钥解密。

1.2.3 RSA工具类

<code class="java">package com.demo.utils;

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

    public static Map<String, String> generateRasKey() {
        // 生成 RSA 密钥对并返回
    }

    public static String encrypt(String str, String publicKey) {
        // 使用公钥加密
    }

    public static String decrypt(String str, String privateKey) {
        // 使用私钥解密
    }
}
</code>

1.2.4 UT

<code class="java">@Getter
public class RsaException extends RuntimeException {
    private final String message;
    public RsaException(String message) { this.message = message; }
}
</code>

1.3 案例

使用 Spring Cloud Gateway、SpringBoot、Nacos、Redis 实现安全登录和查询。

1.3.1 前端登录代码

<code class="html"><!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>登录页面</title></head>
<body>
<h1>登录</h1>
<form id="from">
    账号:<input id="username" type="text" />
    密码:<input id="password" type="password" />
    <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>
    var encrypt = new JSEncrypt();
    encrypt.setPublicKey("YOUR_PUBLIC_KEY");
    $("#btn_login").click(function(){
        const username = $("#username").val();
        const password = $("#password").val();
        const form = {username, password};
        const timestamp = Date.now();
        const requestId = getUuid();
        const data = JSON.stringify(sort_ASCII(form));
        const 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",
            dataType: "json",
            contentType: "application/json;charset=utf-8",
            success: function(data){ console.log(data); }
        });
    });
    function getUuid(){ /* 生成 UUID */ }
    function sort_ASCII(obj){ /* ASCII 升序排序 */ }
</script>
</body>
</html>
</code>

1.3.2 后端业务代码

<code class="java">@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public String login(@RequestBody UserForm userForm) {
        return userService.login(userForm);
    }

    @GetMapping("/detail")
    public JSONObject detail(@RequestParam("id") Long id) {
        return userService.detail(id);
    }
}

@Service
public class UserService {
    private static final String USERNAME = "admin";
    private static final String PASSWORD = "123456";
    private static final Long USER_ID = 1L;

    public String login(UserForm userForm) {
        if (USERNAME.equals(userForm.getUsername()) && PASSWORD.equals(userForm.getPassword())) {
            JSONObject userInfo = new JSONObject();
            userInfo.put("username", USERNAME);
            userInfo.put("password", PASSWORD);
            userInfo.put("userId", USER_ID);
            return TokenUtils.createToken(userInfo.toJSONString());
        }
        return "账号密码不正确";
    }

    public JSONObject detail(Long id) {
        JSONObject json = new JSONObject();
        json.put("id", id);
        json.put("name", "admin");
        return json;
    }
}
</code>

2 设置 URL 有效时长

在请求 Header 中加入时间戳,后端校验时间戳是否在 5 分钟内。

<code class="java">private Long getDateTimestamp(HttpHeaders httpHeaders) {
    List<String> list = httpHeaders.get("timestamp");
    if (CollectionUtils.isEmpty(list)) {
        throw new IllegalArgumentException("拒绝服务");
    }
    long timestamp = Long.parseLong(list.get(0));
    long now = System.currentTimeMillis();
    if (now - timestamp > 1000 * 60 * 5) {
        throw new IllegalArgumentException("拒绝服务");
    }
    return timestamp;
}
</code>

3 确保 URL 唯一性

前端在请求中加入 UUID,后端将 UUID 存入 Redis,5 分钟内重复请求即被拒绝。

<code class="java">private String getRequestId(HttpHeaders headers) {
    List<String> list = headers.get("requestId");
    if (CollectionUtils.isEmpty(list)) {
        throw new IllegalArgumentException("拒绝服务");
    }
    String requestId = list.get(0);
    String temp = redisTemplate.opsForValue().get(requestId);
    if (StringUtils.isNotBlank(temp)) {
        throw new IllegalArgumentException("拒绝服务");
    }
    redisTemplate.opsForValue().set(requestId, requestId, 5, TimeUnit.MINUTES);
    return requestId;
}
</code>

4 增加签名

前端对请求参数(按 ASCII 升序)+ UUID + 时间戳进行 MD5 加密生成签名,后端校验签名一致性。

<code class="java">private void checkSign(String sign, Long timestamp, String requestId, Map<String, Object> paramMap) {
    String data = JSON.toJSONString(paramMap) + requestId + timestamp;
    String expected = Md5Utils.getMD5(data.getBytes());
    if (!expected.equals(sign)) {
        throw new IllegalArgumentException("拒绝服务");
    }
}
</code>

GatewayFilterConfig(核心实现)

<code class="java">@Component
public class GatewayFilterConfig implements GlobalFilter, Ordered {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Long timestamp = getDateTimestamp(exchange.getRequest().getHeaders());
        String requestId = getRequestId(exchange.getRequest().getHeaders());
        String sign = getSign(exchange.getRequest().getHeaders());
        // token 校验(登录除外)
        String requestUrl = exchange.getRequest().getPath().value();
        AntPathMatcher matcher = new AntPathMatcher();
        if (!matcher.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);
            }
        }
        // 读取并解密请求体
        ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
            .flatMap(body -> {
                String plain = RSAUtils.decrypt(body, RSAConstant.PRIVATE_KEY);
                JSONObject json = JSON.parseObject(plain);
                Map<String, Object> paramMap = new TreeMap<>();
                json.forEach((k,v) -> paramMap.put(k, v));
                checkSign(sign, timestamp, requestId, paramMap);
                return Mono.just(plain);
            });
        BodyInserter<Mono<String>, 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, timestamp);
        return inserter.insert(outputMessage, new BodyInserterContext())
            .then(Mono.defer(() -> {
                ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                    @Override
                    public Flux<DataBuffer> getBody() {
                        Flux<DataBuffer> 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());
            }));
    }

    // 省略 getSign、getDateTimestamp、getRequestId 等辅助方法实现(同上)

    @Override
    public int getOrder() { return 80; }
}
</code>

通过上述步骤,系统实现了请求数据的加密传输、时间戳防重放、UUID 防重复提交以及签名完整性校验,显著提升了 Spring Cloud Gateway 的安全性。

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaRSAsignatureSpring Cloud Gateway
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

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.