fix(gateway): include stable request and trace ids in security errors
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.gateway.config;
|
package com.baiyue.gateway.config;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.baiyue.gateway.filter.CorrelationIdWebFilter;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -69,9 +70,14 @@ public class GatewaySecurityConfig {
|
|||||||
ObjectMapper mapper, HttpStatus status, String code, String message) {
|
ObjectMapper mapper, HttpStatus status, String code, String message) {
|
||||||
exchange.getResponse().setStatusCode(status);
|
exchange.getResponse().setStatusCode(status);
|
||||||
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
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<String, Object> body = new LinkedHashMap<>();
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
body.put("code", code); body.put("message", message); body.put("data", null);
|
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 {
|
try {
|
||||||
byte[] bytes = mapper.writeValueAsBytes(body);
|
byte[] bytes = mapper.writeValueAsBytes(body);
|
||||||
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(bytes)));
|
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(bytes)));
|
||||||
|
|||||||
@@ -14,19 +14,36 @@ import java.util.UUID;
|
|||||||
@Component
|
@Component
|
||||||
public class CorrelationIdWebFilter implements WebFilter, Ordered {
|
public class CorrelationIdWebFilter implements WebFilter, Ordered {
|
||||||
public static final String HEADER = "X-Request-Id";
|
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
|
@Override
|
||||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||||
String requestId = validRequestId(exchange.getRequest().getHeaders())
|
String requestId = ensureRequestId(exchange);
|
||||||
? exchange.getRequest().getHeaders().getFirst(HEADER)
|
String traceId = ensureTraceId(exchange);
|
||||||
: UUID.randomUUID().toString();
|
|
||||||
exchange.getResponse().getHeaders().set(HEADER, requestId);
|
exchange.getResponse().getHeaders().set(HEADER, requestId);
|
||||||
|
exchange.getResponse().getHeaders().set(TRACE_HEADER, traceId);
|
||||||
return chain.filter(exchange);
|
return chain.filter(exchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean validRequestId(HttpHeaders headers) {
|
public static String ensureRequestId(ServerWebExchange exchange) {
|
||||||
String value = headers.getFirst(HEADER);
|
return (String) exchange.getAttributes().computeIfAbsent(REQUEST_ID_ATTRIBUTE, key -> {
|
||||||
return value != null && !value.isBlank() && value.length() <= 128
|
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);
|
&& value.chars().allMatch(character -> character >= 0x21 && character <= 0x7e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user