修复 SuperAgent Open API CSRF 请求头
This commit is contained in:
@@ -179,6 +179,7 @@
|
|||||||
| `SUPERAGENT_DEBUG_EML_CONNECT_TIMEOUT` | 否 | SuperAgent Open API 建连超时,默认 `15s`。 |
|
| `SUPERAGENT_DEBUG_EML_CONNECT_TIMEOUT` | 否 | SuperAgent Open API 建连超时,默认 `15s`。 |
|
||||||
| `SUPERAGENT_DEBUG_EML_READ_TIMEOUT` | 否 | 旧版 RestClient 读取超时兼容变量;当前 JDK SSE 客户端不设置整段 SSE 固定读取超时,断流恢复由 run/events 机制处理。 |
|
| `SUPERAGENT_DEBUG_EML_READ_TIMEOUT` | 否 | 旧版 RestClient 读取超时兼容变量;当前 JDK SSE 客户端不设置整段 SSE 固定读取超时,断流恢复由 run/events 机制处理。 |
|
||||||
| `SUPERAGENT_OPEN_API_SSE_RECOVERY_MAX_ATTEMPTS` | 否 | SSE EOF 后通过 `/runs/{run_id}/events` 恢复的最大尝试次数,默认 `5`。 |
|
| `SUPERAGENT_OPEN_API_SSE_RECOVERY_MAX_ATTEMPTS` | 否 | SSE EOF 后通过 `/runs/{run_id}/events` 恢复的最大尝试次数,默认 `5`。 |
|
||||||
|
| SuperAgent Open API CSRF | 否 | 不需要额外环境变量;后端每次请求自动生成临时 `X-CSRF-Token`,并用同值 `csrf_token` Cookie 做 double-submit。 |
|
||||||
| `ALIYUN_OSS_DEV_ENDPOINT` / `ALIYUN_OSS_TEST_ENDPOINT` / `ALIYUN_OSS_PROD_ENDPOINT` | 否 | 阿里云 OSS Endpoint,未配置时可兜底 `ALIYUN_OSS_ENDPOINT`。 |
|
| `ALIYUN_OSS_DEV_ENDPOINT` / `ALIYUN_OSS_TEST_ENDPOINT` / `ALIYUN_OSS_PROD_ENDPOINT` | 否 | 阿里云 OSS Endpoint,未配置时可兜底 `ALIYUN_OSS_ENDPOINT`。 |
|
||||||
| `ALIYUN_OSS_DEV_BUCKET` / `ALIYUN_OSS_TEST_BUCKET` / `ALIYUN_OSS_PROD_BUCKET` | 否 | 阿里云 OSS Bucket,未配置时可兜底 `ALIYUN_OSS_BUCKET`。 |
|
| `ALIYUN_OSS_DEV_BUCKET` / `ALIYUN_OSS_TEST_BUCKET` / `ALIYUN_OSS_PROD_BUCKET` | 否 | 阿里云 OSS Bucket,未配置时可兜底 `ALIYUN_OSS_BUCKET`。 |
|
||||||
| `ALIYUN_OSS_DEV_ACCESS_KEY_ID` / `ALIYUN_OSS_TEST_ACCESS_KEY_ID` / `ALIYUN_OSS_PROD_ACCESS_KEY_ID` | 是 | 阿里云 OSS AccessKey ID,未配置时可兜底 `ALIYUN_OSS_ACCESS_KEY_ID`。 |
|
| `ALIYUN_OSS_DEV_ACCESS_KEY_ID` / `ALIYUN_OSS_TEST_ACCESS_KEY_ID` / `ALIYUN_OSS_PROD_ACCESS_KEY_ID` | 是 | 阿里云 OSS AccessKey ID,未配置时可兜底 `ALIYUN_OSS_ACCESS_KEY_ID`。 |
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
- Debug EML 第一版只展示 SuperAgent 结果,不创建订单、不创建任务、不调用任务结果通知接口。
|
- Debug EML 第一版只展示 SuperAgent 结果,不创建订单、不创建任务、不调用任务结果通知接口。
|
||||||
- AgentBus 实时收到邮件后自动推 SuperAgent 已由 M007 后端 V1 实现,不能把 Debug EML 链路等同于生产实时自动处理链路。
|
- AgentBus 实时收到邮件后自动推 SuperAgent 已由 M007 后端 V1 实现,不能把 Debug EML 链路等同于生产实时自动处理链路。
|
||||||
- Debug EML 和 AgentBus 自动分发复用同一个 SuperAgent Open API SSE 稳定客户端;上线前必须验证 `run.completed + end + final answer` 严格成功条件和 EOF 后 `/events` 恢复。
|
- Debug EML 和 AgentBus 自动分发复用同一个 SuperAgent Open API SSE 稳定客户端;上线前必须验证 `run.completed + end + final answer` 严格成功条件和 EOF 后 `/events` 恢复。
|
||||||
|
- 当前共享 Open API client 会自动携带临时 CSRF double-submit header / cookie;如果测试机仍返回 `CSRF token missing`,优先检查部署包版本和反向代理是否转发 `X-CSRF-Token`、`Cookie`。
|
||||||
|
|
||||||
## 4. 数据库上线注意事项
|
## 4. 数据库上线注意事项
|
||||||
|
|
||||||
|
|||||||
@@ -143,14 +143,16 @@ POST /api/open/agent-sessions
|
|||||||
|
|
||||||
2026-07-12 Open API 文档中,Java SSE 调用使用 `Authorization: Bearer <DEERFLOW_OPEN_API_KEY>` 或
|
2026-07-12 Open API 文档中,Java SSE 调用使用 `Authorization: Bearer <DEERFLOW_OPEN_API_KEY>` 或
|
||||||
`X-DeerFlow-Open-API-Key` 鉴权,并通过 `X-Request-ID`、`idempotency_key` 和 metadata 做调用关联。
|
`X-DeerFlow-Open-API-Key` 鉴权,并通过 `X-Request-ID`、`idempotency_key` 和 metadata 做调用关联。
|
||||||
当前 TH Hotel 后端 `SuperAgentOpenApiClientImpl` 不发送 CSRF double-submit。
|
当前 TH Hotel 后端 `SuperAgentOpenApiClientImpl` 已发送 CSRF double-submit。CSRF token 由后端每次请求临时生成,不走环境变量,不作为长期 Secret 保存。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Authorization: Bearer <DEERFLOW_OPEN_API_KEY>
|
Authorization: Bearer <DEERFLOW_OPEN_API_KEY>
|
||||||
X-Request-ID: <stable-request-id>
|
X-Request-ID: <stable-request-id>
|
||||||
|
X-CSRF-Token: <temporary-random-token>
|
||||||
|
Cookie: csrf_token=<same-temporary-random-token>
|
||||||
```
|
```
|
||||||
|
|
||||||
如果 SuperAgent 服务端后续重新要求 CSRF double-submit,应先更新本文和 Open API client,再开启 M007 worker。
|
Debug EML 和 M007 AgentBus 自动分发复用同一个 Open API client,因此两条链路都会携带上述 CSRF header / cookie。
|
||||||
|
|
||||||
### 4.3 Session 请求示例
|
### 4.3 Session 请求示例
|
||||||
|
|
||||||
@@ -680,7 +682,7 @@ SuperAgent 返回的是 Provider 输出。即使未来返回结构化 JSON,也
|
|||||||
接入 SuperAgent 前确认:
|
接入 SuperAgent 前确认:
|
||||||
|
|
||||||
- [ ] 已获得 Open API Key 和允许访问的 Base URL。
|
- [ ] 已获得 Open API Key 和允许访问的 Base URL。
|
||||||
- [ ] 已确认当前 Open API 鉴权方式;如需 CSRF double-submit,已同步更新后端 client。
|
- [ ] 已确认当前 Open API 鉴权方式;当前后端 client 会自动发送临时 CSRF double-submit header / cookie。
|
||||||
- [ ] 已确认 Session、Message、Run 的生命周期。
|
- [ ] 已确认 Session、Message、Run 的生命周期。
|
||||||
- [ ] 已确认 SSE 最终答案或结构化结果所在字段。
|
- [ ] 已确认 SSE 最终答案或结构化结果所在字段。
|
||||||
- [ ] 已定义 `AgentCapabilityPort` 和调用审计表。
|
- [ ] 已定义 `AgentCapabilityPort` 和调用审计表。
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ POST /api/open/agent-sessions/{sessionId}/messages/stream
|
|||||||
第一版建议:
|
第一版建议:
|
||||||
|
|
||||||
- 后端使用 `DEERFLOW_BASE_URL` 和 `DEERFLOW_OPEN_API_KEY` 调用 SuperAgent。
|
- 后端使用 `DEERFLOW_BASE_URL` 和 `DEERFLOW_OPEN_API_KEY` 调用 SuperAgent。
|
||||||
- 状态变更请求使用 CSRF double-submit:`X-CSRF-Token` 和 `Cookie: csrf_token=<same-token>`。
|
- 状态变更请求使用 CSRF double-submit:`X-CSRF-Token` 和 `Cookie: csrf_token=<same-token>`;当前共享 Open API client 已自动生成临时随机 token 并同时写入 header / cookie。
|
||||||
- 创建 session 时使用 `SUPERAGENT_DEBUG_EML_EXTERNAL_SUBJECT_ID` 作为 `external_subject_id`。
|
- 创建 session 时使用 `SUPERAGENT_DEBUG_EML_EXTERNAL_SUBJECT_ID` 作为 `external_subject_id`。
|
||||||
- `idempotency_key` 使用 `debug_run_id` 派生,保证同一次 Debug 运行不会重复创建不可追溯请求。
|
- `idempotency_key` 使用 `debug_run_id` 派生,保证同一次 Debug 运行不会重复创建不可追溯请求。
|
||||||
- 发送消息时把 AgentBus-like payload 序列化为 JSON 文本,并附加中文指令,要求 SuperAgent 输出结构化 JSON。
|
- 发送消息时把 AgentBus-like payload 序列化为 JSON 文本,并附加中文指令,要求 SuperAgent 输出结构化 JSON。
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ AgentBus WebSocket 收到邮件
|
|||||||
2026-07-12 导入的 `OPEN_AGENT_API_JAVA_SSE_CLIENT.md` 对 Java SSE 调用提出新的强约束。本项目后续 Open API client 必须满足:
|
2026-07-12 导入的 `OPEN_AGENT_API_JAVA_SSE_CLIENT.md` 对 Java SSE 调用提出新的强约束。本项目后续 Open API client 必须满足:
|
||||||
|
|
||||||
- 请求 `messages/stream?include_trace=true` 时生成稳定 `X-Request-ID`。
|
- 请求 `messages/stream?include_trace=true` 时生成稳定 `X-Request-ID`。
|
||||||
|
- Open API 请求必须携带 CSRF double-submit:`X-CSRF-Token` 与 `Cookie: csrf_token=<same-token>` 使用后端临时随机值,不写入配置文件或环境变量。
|
||||||
- 同一业务 SourceMessage 的 `idempotency_key` 在所有尝试中保持不变。
|
- 同一业务 SourceMessage 的 `idempotency_key` 在所有尝试中保持不变。
|
||||||
- 初始 POST 成功后保存响应头 `Content-Location`,解析并保存 SuperAgent `run_id`。
|
- 初始 POST 成功后保存响应头 `Content-Location`,解析并保存 SuperAgent `run_id`。
|
||||||
- SSE 必须按帧解析 `event:`、`data:`、`id:` 和 heartbeat comment。
|
- SSE 必须按帧解析 `event:`、`data:`、`id:` 和 heartbeat comment。
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
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.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -27,6 +29,11 @@ import org.springframework.stereotype.Service;
|
|||||||
@Service
|
@Service
|
||||||
public class SuperAgentOpenApiClientImpl implements SuperAgentOpenApiClient {
|
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 SuperAgentOpenApiProperties properties;
|
||||||
private final SuperAgentOpenApiSseParser sseParser;
|
private final SuperAgentOpenApiSseParser sseParser;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
@@ -198,15 +205,27 @@ public class SuperAgentOpenApiClientImpl implements SuperAgentOpenApiClient {
|
|||||||
* 构造 SuperAgent Open API 通用请求头,不记录或返回 API Key。
|
* 构造 SuperAgent Open API 通用请求头,不记录或返回 API Key。
|
||||||
*/
|
*/
|
||||||
private HttpRequest.Builder baseRequest(URI uri, String requestId) {
|
private HttpRequest.Builder baseRequest(URI uri, String requestId) {
|
||||||
|
String csrfToken = newCsrfToken();
|
||||||
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
|
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
|
||||||
.header("Authorization", "Bearer " + properties.getApiKey())
|
.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)) {
|
if (!blank(requestId)) {
|
||||||
builder.header("X-Request-ID", requestId);
|
builder.header("X-Request-ID", requestId);
|
||||||
}
|
}
|
||||||
return builder;
|
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。
|
* 校验 HTTP 状态码,错误摘要不包含 Secret。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -42,7 +42,13 @@ class SuperAgentOpenApiClientImplTest {
|
|||||||
void shouldRequestStreamingMessagesWithIncludeTraceAndEmitPublicTraceEvents() throws Exception {
|
void shouldRequestStreamingMessagesWithIncludeTraceAndEmitPublicTraceEvents() throws Exception {
|
||||||
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
|
HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
|
||||||
AtomicReference<String> streamQuery = new AtomicReference<>();
|
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 -> {
|
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);
|
byte[] response = "{\"session_id\":\"session-http-001\"}".getBytes(StandardCharsets.UTF_8);
|
||||||
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||||
exchange.sendResponseHeaders(200, response.length);
|
exchange.sendResponseHeaders(200, response.length);
|
||||||
@@ -52,6 +58,8 @@ class SuperAgentOpenApiClientImplTest {
|
|||||||
});
|
});
|
||||||
server.createContext("/api/open/agent-sessions/session-http-001/messages/stream", exchange -> {
|
server.createContext("/api/open/agent-sessions/session-http-001/messages/stream", exchange -> {
|
||||||
streamQuery.set(exchange.getRequestURI().getRawQuery());
|
streamQuery.set(exchange.getRequestURI().getRawQuery());
|
||||||
|
streamCsrfToken.set(exchange.getRequestHeaders().getFirst("X-CSRF-Token"));
|
||||||
|
streamCookie.set(exchange.getRequestHeaders().getFirst("Cookie"));
|
||||||
byte[] response = """
|
byte[] response = """
|
||||||
id: 1
|
id: 1
|
||||||
event: trace
|
event: trace
|
||||||
@@ -98,6 +106,10 @@ class SuperAgentOpenApiClientImplTest {
|
|||||||
Map.of("source", "unit-test")), traceEvents::add);
|
Map.of("source", "unit-test")), traceEvents::add);
|
||||||
|
|
||||||
assertThat(streamQuery.get()).isEqualTo("include_trace=true");
|
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.sessionId()).isEqualTo("session-http-001");
|
||||||
assertThat(result.runId()).isEqualTo("run-http-001");
|
assertThat(result.runId()).isEqualTo("run-http-001");
|
||||||
assertThat(result.lastEventId()).isEqualTo("4");
|
assertThat(result.lastEventId()).isEqualTo("4");
|
||||||
|
|||||||
Reference in New Issue
Block a user