From 2de3fb36e9af5977a051cd11ea4104577a59ad27 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 11 Aug 2026 10:35:01 +0800 Subject: [PATCH] fix(gateway): include stable request and trace ids in security errors --- .../gateway/config/GatewaySecurityConfig.java | 8 ++- .../filter/CorrelationIdWebFilter.java | 29 ++++++++--- .../GatewaySecurityErrorResponseTest.java | 49 +++++++++++++++++++ .../filter/CorrelationIdWebFilterTest.java | 27 ++++++++++ 4 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 gateway/src/test/java/com/baiyue/gateway/config/GatewaySecurityErrorResponseTest.java create mode 100644 gateway/src/test/java/com/baiyue/gateway/filter/CorrelationIdWebFilterTest.java diff --git a/gateway/src/main/java/com/baiyue/gateway/config/GatewaySecurityConfig.java b/gateway/src/main/java/com/baiyue/gateway/config/GatewaySecurityConfig.java index f6b2546a4..8d61b7730 100644 --- a/gateway/src/main/java/com/baiyue/gateway/config/GatewaySecurityConfig.java +++ b/gateway/src/main/java/com/baiyue/gateway/config/GatewaySecurityConfig.java @@ -1,6 +1,7 @@ package com.baiyue.gateway.config; import com.fasterxml.jackson.databind.ObjectMapper; +import com.baiyue.gateway.filter.CorrelationIdWebFilter; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -69,9 +70,14 @@ public class GatewaySecurityConfig { ObjectMapper mapper, HttpStatus status, String code, String message) { exchange.getResponse().setStatusCode(status); exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + String requestId = CorrelationIdWebFilter.ensureRequestId(exchange); + String traceId = CorrelationIdWebFilter.ensureTraceId(exchange); + exchange.getResponse().getHeaders().set(CorrelationIdWebFilter.HEADER, requestId); + exchange.getResponse().getHeaders().set(CorrelationIdWebFilter.TRACE_HEADER, traceId); Map body = new LinkedHashMap<>(); body.put("code", code); body.put("message", message); body.put("data", null); - body.put("requestId", exchange.getRequest().getId()); body.put("timestamp", Instant.now().toString()); + body.put("requestId", requestId); body.put("traceId", traceId); + body.put("timestamp", Instant.now().toString()); try { byte[] bytes = mapper.writeValueAsBytes(body); return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(bytes))); diff --git a/gateway/src/main/java/com/baiyue/gateway/filter/CorrelationIdWebFilter.java b/gateway/src/main/java/com/baiyue/gateway/filter/CorrelationIdWebFilter.java index 28db1fc37..d29bf982f 100644 --- a/gateway/src/main/java/com/baiyue/gateway/filter/CorrelationIdWebFilter.java +++ b/gateway/src/main/java/com/baiyue/gateway/filter/CorrelationIdWebFilter.java @@ -14,19 +14,36 @@ import java.util.UUID; @Component public class CorrelationIdWebFilter implements WebFilter, Ordered { public static final String HEADER = "X-Request-Id"; + public static final String TRACE_HEADER = "X-Trace-Id"; + public static final String REQUEST_ID_ATTRIBUTE = CorrelationIdWebFilter.class.getName() + ".requestId"; + public static final String TRACE_ID_ATTRIBUTE = CorrelationIdWebFilter.class.getName() + ".traceId"; @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - String requestId = validRequestId(exchange.getRequest().getHeaders()) - ? exchange.getRequest().getHeaders().getFirst(HEADER) - : UUID.randomUUID().toString(); + String requestId = ensureRequestId(exchange); + String traceId = ensureTraceId(exchange); exchange.getResponse().getHeaders().set(HEADER, requestId); + exchange.getResponse().getHeaders().set(TRACE_HEADER, traceId); return chain.filter(exchange); } - private static boolean validRequestId(HttpHeaders headers) { - String value = headers.getFirst(HEADER); - return value != null && !value.isBlank() && value.length() <= 128 + public static String ensureRequestId(ServerWebExchange exchange) { + return (String) exchange.getAttributes().computeIfAbsent(REQUEST_ID_ATTRIBUTE, key -> { + HttpHeaders headers = exchange.getRequest().getHeaders(); + return validRequestId(headers.getFirst(HEADER)) + ? headers.getFirst(HEADER) : UUID.randomUUID().toString(); + }); + } + + public static String ensureTraceId(ServerWebExchange exchange) { + return (String) exchange.getAttributes().computeIfAbsent(TRACE_ID_ATTRIBUTE, key -> { + String incoming = exchange.getRequest().getHeaders().getFirst(TRACE_HEADER); + return validRequestId(incoming) ? incoming : UUID.randomUUID().toString(); + }); + } + + private static boolean validRequestId(String value) { + return value != null && !value.isBlank() && value.length() >= 16 && value.length() <= 128 && value.chars().allMatch(character -> character >= 0x21 && character <= 0x7e); } diff --git a/gateway/src/test/java/com/baiyue/gateway/config/GatewaySecurityErrorResponseTest.java b/gateway/src/test/java/com/baiyue/gateway/config/GatewaySecurityErrorResponseTest.java new file mode 100644 index 000000000..ab3324863 --- /dev/null +++ b/gateway/src/test/java/com/baiyue/gateway/config/GatewaySecurityErrorResponseTest.java @@ -0,0 +1,49 @@ +package com.baiyue.gateway.config; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.http.HttpStatus; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class GatewaySecurityErrorResponseTest { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @ParameterizedTest + @CsvSource({ + "UNAUTHORIZED, A0401", + "FORBIDDEN, A0403" + }) + void securityErrorsExposeStableCompleteCorrelationIds(String statusName, String code) throws Exception { + var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/api/platform/v1/orders").build()); + HttpStatus status = HttpStatus.valueOf(statusName); + + GatewaySecurityConfig.writeError(exchange, objectMapper, status, code, "安全响应").block(); + + DataBuffer buffer = DataBufferUtils.join(exchange.getResponse().getBody()).block(); + assertThat(buffer).isNotNull(); + byte[] bytes = new byte[buffer.readableByteCount()]; + buffer.read(bytes); + DataBufferUtils.release(buffer); + String body = new String(bytes, StandardCharsets.UTF_8); + JsonNode json = objectMapper.readTree(body); + String requestId = json.get("requestId").asText(); + String traceId = json.get("traceId").asText(); + + assertThat(exchange.getResponse().getStatusCode()).isEqualTo(status); + assertThat(json.get("code").asText()).isEqualTo(code); + assertThat(UUID.fromString(requestId)).isNotNull(); + assertThat(UUID.fromString(traceId)).isNotNull(); + assertThat(exchange.getResponse().getHeaders().getFirst("X-Request-Id")).isEqualTo(requestId); + assertThat(exchange.getResponse().getHeaders().getFirst("X-Trace-Id")).isEqualTo(traceId); + } +} diff --git a/gateway/src/test/java/com/baiyue/gateway/filter/CorrelationIdWebFilterTest.java b/gateway/src/test/java/com/baiyue/gateway/filter/CorrelationIdWebFilterTest.java new file mode 100644 index 000000000..1c95246a2 --- /dev/null +++ b/gateway/src/test/java/com/baiyue/gateway/filter/CorrelationIdWebFilterTest.java @@ -0,0 +1,27 @@ +package com.baiyue.gateway.filter; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; + +class CorrelationIdWebFilterTest { + @Test + void replacesShortIncomingRequestIdAndCreatesTraceId() { + var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/api/platform/v1/orders") + .header(CorrelationIdWebFilter.HEADER, "a7612a59-6211") + .build()); + + new CorrelationIdWebFilter().filter(exchange, ignored -> Mono.empty()).block(); + + String requestId = exchange.getResponse().getHeaders().getFirst(CorrelationIdWebFilter.HEADER); + String traceId = exchange.getResponse().getHeaders().getFirst(CorrelationIdWebFilter.TRACE_HEADER); + assertThat((String) requestId).matches("[0-9a-fA-F-]{36}"); + assertThat((String) traceId).matches("[0-9a-fA-F-]{36}"); + assertThat(requestId).isNotEqualTo("a7612a59-6211"); + assertThat((String) exchange.getAttribute(CorrelationIdWebFilter.REQUEST_ID_ATTRIBUTE)).isEqualTo(requestId); + assertThat((String) exchange.getAttribute(CorrelationIdWebFilter.TRACE_ID_ATTRIBUTE)).isEqualTo(traceId); + } +}