修复 SuperAgent Open API CSRF 请求头
This commit is contained in:
@@ -11,10 +11,12 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
@@ -27,6 +29,11 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class SuperAgentOpenApiClientImpl implements SuperAgentOpenApiClient {
|
||||
|
||||
private static final String CSRF_HEADER_NAME = "X-CSRF-Token";
|
||||
private static final String CSRF_COOKIE_NAME = "csrf_token";
|
||||
private static final int CSRF_TOKEN_BYTES = 32;
|
||||
private static final SecureRandom CSRF_RANDOM = new SecureRandom();
|
||||
|
||||
private final SuperAgentOpenApiProperties properties;
|
||||
private final SuperAgentOpenApiSseParser sseParser;
|
||||
private final ObjectMapper objectMapper;
|
||||
@@ -198,15 +205,27 @@ public class SuperAgentOpenApiClientImpl implements SuperAgentOpenApiClient {
|
||||
* 构造 SuperAgent Open API 通用请求头,不记录或返回 API Key。
|
||||
*/
|
||||
private HttpRequest.Builder baseRequest(URI uri, String requestId) {
|
||||
String csrfToken = newCsrfToken();
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
|
||||
.header("Authorization", "Bearer " + properties.getApiKey())
|
||||
.header("Cache-Control", "no-cache");
|
||||
.header("Cache-Control", "no-cache")
|
||||
.header(CSRF_HEADER_NAME, csrfToken)
|
||||
.header("Cookie", CSRF_COOKIE_NAME + "=" + csrfToken);
|
||||
if (!blank(requestId)) {
|
||||
builder.header("X-Request-ID", requestId);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成临时 CSRF double-submit token。该值只用于当前 HTTP 请求,不作为长期 Secret 保存或输出。
|
||||
*/
|
||||
private String newCsrfToken() {
|
||||
byte[] bytes = new byte[CSRF_TOKEN_BYTES];
|
||||
CSRF_RANDOM.nextBytes(bytes);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 HTTP 状态码,错误摘要不包含 Secret。
|
||||
*/
|
||||
|
||||
@@ -42,7 +42,13 @@ class SuperAgentOpenApiClientImplTest {
|
||||
void shouldRequestStreamingMessagesWithIncludeTraceAndEmitPublicTraceEvents() throws Exception {
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
|
||||
AtomicReference<String> streamQuery = new AtomicReference<>();
|
||||
AtomicReference<String> sessionCsrfToken = new AtomicReference<>();
|
||||
AtomicReference<String> sessionCookie = new AtomicReference<>();
|
||||
AtomicReference<String> streamCsrfToken = new AtomicReference<>();
|
||||
AtomicReference<String> streamCookie = new AtomicReference<>();
|
||||
server.createContext("/api/open/agent-sessions", exchange -> {
|
||||
sessionCsrfToken.set(exchange.getRequestHeaders().getFirst("X-CSRF-Token"));
|
||||
sessionCookie.set(exchange.getRequestHeaders().getFirst("Cookie"));
|
||||
byte[] response = "{\"session_id\":\"session-http-001\"}".getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(200, response.length);
|
||||
@@ -52,6 +58,8 @@ class SuperAgentOpenApiClientImplTest {
|
||||
});
|
||||
server.createContext("/api/open/agent-sessions/session-http-001/messages/stream", exchange -> {
|
||||
streamQuery.set(exchange.getRequestURI().getRawQuery());
|
||||
streamCsrfToken.set(exchange.getRequestHeaders().getFirst("X-CSRF-Token"));
|
||||
streamCookie.set(exchange.getRequestHeaders().getFirst("Cookie"));
|
||||
byte[] response = """
|
||||
id: 1
|
||||
event: trace
|
||||
@@ -98,6 +106,10 @@ class SuperAgentOpenApiClientImplTest {
|
||||
Map.of("source", "unit-test")), traceEvents::add);
|
||||
|
||||
assertThat(streamQuery.get()).isEqualTo("include_trace=true");
|
||||
assertThat(sessionCsrfToken.get()).isNotBlank();
|
||||
assertThat(sessionCookie.get()).contains("csrf_token=" + sessionCsrfToken.get());
|
||||
assertThat(streamCsrfToken.get()).isNotBlank();
|
||||
assertThat(streamCookie.get()).contains("csrf_token=" + streamCsrfToken.get());
|
||||
assertThat(result.sessionId()).isEqualTo("session-http-001");
|
||||
assertThat(result.runId()).isEqualTo("run-http-001");
|
||||
assertThat(result.lastEventId()).isEqualTo("4");
|
||||
|
||||
Reference in New Issue
Block a user