判定接线(SignVerifyFilter 拆三段,内部入口逻辑逐字节不变):
- 内部入口(容器网直连,无标记)一律验签,不看 visibility/authMode
- 外部入口(来源可信 + X-Entry: external)→ INTERNAL 裸 403;EXTERNAL×SIGNED 验签;
EXTERNAL×ANON 限流内免签放行(verdict=ANON / caller=anonymous / 不注入身份头),
超限 429、限流未就绪 403(fail-closed)
- 外部入口的 403/429 属策略层拒绝,与 observe 正交
新增:
- auth/EntryDetector:可信来源 CIDR + X-Entry 双条件判定,仅认 IPv4,非法一律不可信
- auth/AnonRateLimiter:ALLOW/EXCEEDED/UNAVAILABLE 契约
- auth/UnavailableAnonRateLimiter:T3.1 前的占位实现,恒 fail-closed
- route/RouteChangeBroadcaster:频道 gw:route:version 变更广播(判定仍读本地 Caffeine,
Redis 只做变更通知,Redis 异常回落本地快照,不存在“挂了就放行”)
其他:
- GatewayProperties 新增 trusted-proxies / anon-per-ip-qps
- StreamingForwarder 把 x-entry 加入剥除集(伪造值也到不了提供方)
- InternalAdminController 白名单 {SIGNED,ANON}、删 page()、写后广播;RouteRefreshController 广播
- 硬删 static/admin.html(治理面已前端化,重载注册表由 front 头部按钮承接)
- 测试:EntryDetectorTest、SignVerifyFilterEntryTest(入口×策略矩阵)等,101/101 全绿
This commit is contained in:
@@ -28,13 +28,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 治理小页后端(N02 TDD):page/routes/targets/logs 查询形态 + 路由/目标行内编辑的
|
||||
* 治理面后端(N02 TDD):routes/targets/logs 查询形态 + 路由/目标行内编辑的
|
||||
* 值域闸(非法值不落库)+ 写后热重载调用。MockMvc standalone + mock 仓储/注册表。
|
||||
* <p>
|
||||
* <b>契约说明(B3/B4 后)</b>:查询接口返回 {@code BasePageResult}({@code {total, content, ...}}),
|
||||
* 非法入参返回 <b>HTTP 200</b> + {@code {code:2, msg}}(平台规范)。standalone 模式不装配
|
||||
* {@code yh-api-std} 的 {@code ApiResponseAdvice},故成功响应在本测试中保持裸结构;
|
||||
* 线上由 advice 统一再包一层 {@code {code,msg,data}}(分页体落在 {@code data})。
|
||||
* <p>
|
||||
* v1 内置 HTML({@code /api/internal/admin/page} + static/admin.html)已退役:
|
||||
* 治理 UI 独立在 {@code open-gateway/front},故此处不再有用例。
|
||||
*/
|
||||
class InternalAdminControllerTest {
|
||||
|
||||
@@ -65,16 +68,6 @@ class InternalAdminControllerTest {
|
||||
.thenReturn(new PageImpl<>(Arrays.asList(logRow())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void page_servesHtml() throws Exception {
|
||||
mvc.perform(get("/api/internal/admin/page"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(result -> {
|
||||
String body = result.getResponse().getContentAsString();
|
||||
org.assertj.core.api.Assertions.assertThat(body).contains("<html");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void routes_listSorted() throws Exception {
|
||||
mvc.perform(get("/api/internal/admin/routes"))
|
||||
@@ -99,7 +92,7 @@ class InternalAdminControllerTest {
|
||||
when(routeRepo.findById(1L)).thenReturn(java.util.Optional.of(route(1L)));
|
||||
mvc.perform(put("/api/internal/admin/routes/1")
|
||||
.contentType("application/json")
|
||||
.content("{\"authMode\":\"ANONYMOUS\",\"cacheTtlSeconds\":30,\"enabled\":true}"))
|
||||
.content("{\"authMode\":\"ANON\",\"cacheTtlSeconds\":30,\"enabled\":true}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.loaded").value(22));
|
||||
verify(routeRepo).save(any(OpenApiRoute.class));
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.yunhai.platform.gateway.auth;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 入口判定用例(T4.3):外部入口 = 来源可信(CIDR 命中)<b>且</b> 带 X-Entry: external。
|
||||
* 覆盖 CIDR 边界、映射地址形态、非法/空缺配置的 fail-closed 行为——伪造 X-Entry(来源不可信)
|
||||
* 必须无效,这是"客户端不能自封为外部入口"的唯一保证(A6)。
|
||||
*/
|
||||
class EntryDetectorTest {
|
||||
|
||||
private static final List<String> DOCKER_NET = Arrays.asList("172.16.0.0/12");
|
||||
|
||||
private static MockHttpServletRequest request(String remoteAddr, String entryHeader) {
|
||||
MockHttpServletRequest req = new MockHttpServletRequest("GET", "/userbase/oapi/x");
|
||||
req.setRemoteAddr(remoteAddr);
|
||||
if (entryHeader != null) {
|
||||
req.addHeader(EntryDetector.HEADER_ENTRY, entryHeader);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
@Test
|
||||
void 可信来源带标记_判定外部入口() {
|
||||
assertThat(EntryDetector.isExternal(request("172.18.0.2", "external"), DOCKER_NET)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 标记大小写与首尾空白不敏感() {
|
||||
assertThat(EntryDetector.isExternal(request("172.18.0.2", " EXTERNAL "), DOCKER_NET)).isTrue();
|
||||
assertThat(EntryDetector.isExternal(request("172.18.0.2", "External"), DOCKER_NET)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 来源不可信_伪造标记无效() {
|
||||
// 公网来源自带 X-Entry: external —— 必须仍按内部入口处理
|
||||
assertThat(EntryDetector.isExternal(request("203.0.113.9", "external"), DOCKER_NET)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 无标记或有其他标记_仍为内部入口() {
|
||||
assertThat(EntryDetector.isExternal(request("172.18.0.2", null), DOCKER_NET)).isFalse();
|
||||
assertThat(EntryDetector.isExternal(request("172.18.0.2", "internal"), DOCKER_NET)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 信任列表为空_不存在外部入口() {
|
||||
assertThat(EntryDetector.isExternal(request("172.18.0.2", "external"), new ArrayList<>())).isFalse();
|
||||
assertThat(EntryDetector.isExternal(request("172.18.0.2", "external"), null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cidr边界_网段内命中网段外不命中() {
|
||||
assertThat(EntryDetector.inAnyCidr("172.16.0.0", DOCKER_NET)).as("起始地址").isTrue();
|
||||
assertThat(EntryDetector.inAnyCidr("172.31.255.255", DOCKER_NET)).as("末地址").isTrue();
|
||||
assertThat(EntryDetector.inAnyCidr("172.15.255.255", DOCKER_NET)).as("前一位").isFalse();
|
||||
assertThat(EntryDetector.inAnyCidr("172.32.0.1", DOCKER_NET)).as("后一位").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 精确地址与全匹配与斜零前缀() {
|
||||
assertThat(EntryDetector.inAnyCidr("10.0.0.7", Arrays.asList("10.0.0.7"))).as("无 / 按精确地址").isTrue();
|
||||
assertThat(EntryDetector.inAnyCidr("10.0.0.8", Arrays.asList("10.0.0.7"))).isFalse();
|
||||
assertThat(EntryDetector.inAnyCidr("8.8.8.8", Arrays.asList("0.0.0.0/0"))).as("/0 全通").isTrue();
|
||||
assertThat(EntryDetector.inAnyCidr("10.1.2.3", Arrays.asList("10.0.0.0/8", "10.1.0.0/16"))).as("多值命中一项即可").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 列表中非法项不影响合法项命中() {
|
||||
assertThat(EntryDetector.inAnyCidr("172.18.0.2", Arrays.asList("bogus", "172.16.0.0/12"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 映射地址形态按IPv4匹配() {
|
||||
assertThat(EntryDetector.inAnyCidr("::ffff:172.18.0.2", DOCKER_NET)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 非法配置与IPv6一律不可信() {
|
||||
assertThat(EntryDetector.inAnyCidr("172.18.0.2", Arrays.asList("172.16.0.0/33"))).as("前缀越界").isFalse();
|
||||
assertThat(EntryDetector.inAnyCidr("172.18.0.2", Arrays.asList("172.16.0.0/abc"))).as("前缀非数字").isFalse();
|
||||
assertThat(EntryDetector.inAnyCidr("172.18.0.2", Arrays.asList("172.16.0"))).as("非法网络段").isFalse();
|
||||
assertThat(EntryDetector.inAnyCidr("172.18.0.2", Arrays.asList(" "))).as("空白项").isFalse();
|
||||
assertThat(EntryDetector.inAnyCidr("fe80::1", Arrays.asList("::/0"))).as("纯 IPv6 不支持").isFalse();
|
||||
assertThat(EntryDetector.inAnyCidr(null, DOCKER_NET)).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.yunhai.platform.gateway.filter;
|
||||
|
||||
import com.yunhai.platform.gateway.auth.AnonRateLimiter;
|
||||
import com.yunhai.platform.gateway.auth.EntryDetector;
|
||||
import com.yunhai.platform.gateway.auth.GatewayKeyService;
|
||||
import com.yunhai.platform.gateway.route.GatewayProperties;
|
||||
import com.yunhai.platform.gateway.route.RouteSpec;
|
||||
import com.yunhai.platform.utils.crypto.SignUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 入口×策略判定矩阵用例(T4.3 / 落地总纲 §0.1,验收 A1-A6):
|
||||
* <ul>
|
||||
* <li>A1 内部入口(无标记)—— 任意路由一律验签,不看 {@code visibility}/{@code authMode};</li>
|
||||
* <li>A2 外部入口打 {@code INTERNAL} 路由 —— 裸 403(且不因 observe=true 而放行);</li>
|
||||
* <li>A3 外部入口打 {@code EXTERNAL×SIGNED} 无签名 —— 裸 401;</li>
|
||||
* <li>A4 外部入口打 {@code EXTERNAL×ANON} 限流内 —— 免签放行,verdict=ANON、不注入身份头;</li>
|
||||
* <li>A5 同上超限 —— 裸 429;限流未就绪 —— 裸 403(fail-closed,D4);</li>
|
||||
* <li>A6 来源不可信时伪造 {@code X-Entry: external} —— 无效,仍按内部入口处理。</li>
|
||||
* </ul>
|
||||
*/
|
||||
class SignVerifyFilterEntryTest {
|
||||
|
||||
private static final String SECRET = "sec-entry-0123456789abcdef";
|
||||
|
||||
private static final RouteSpec INTERNAL_SIGNED = new RouteSpec("INTERNAL", "SIGNED", 0, "CALLER", 0);
|
||||
private static final RouteSpec EXTERNAL_SIGNED = new RouteSpec("EXTERNAL", "SIGNED", 0, "CALLER", 0);
|
||||
private static final RouteSpec EXTERNAL_ANON = new RouteSpec("EXTERNAL", "ANON", 0, "CALLER", 0);
|
||||
|
||||
private static final AnonRateLimiter ALLOW_LIMITER = (ip, qps) -> AnonRateLimiter.Decision.ALLOW;
|
||||
private static final AnonRateLimiter EXCEED_LIMITER = (ip, qps) -> AnonRateLimiter.Decision.EXCEEDED;
|
||||
|
||||
private SignVerifyFilter filter(boolean observe, boolean sourceTrusted, AnonRateLimiter limiter) {
|
||||
SignVerifyFilter f = new SignVerifyFilter();
|
||||
GatewayKeyService keyService = mock(GatewayKeyService.class);
|
||||
when(keyService.getSecret("app1")).thenReturn(SECRET);
|
||||
GatewayProperties props = new GatewayProperties();
|
||||
props.setObserve(observe);
|
||||
props.setTrustedProxies(sourceTrusted ? Arrays.asList("0.0.0.0/0") : new ArrayList<>());
|
||||
f.setKeyService(keyService);
|
||||
f.setProperties(props);
|
||||
f.setAnonRateLimiter(limiter);
|
||||
return f;
|
||||
}
|
||||
|
||||
/** 已过 RoutingFilter 的请求:routeSpec + 剥段 path;remoteAddr 默认 127.0.0.1(落在 0.0.0.0/0 内) */
|
||||
private MockHttpServletRequest request(RouteSpec spec, String xEntry) {
|
||||
MockHttpServletRequest req = new MockHttpServletRequest("GET", "/userbase/oapi/message/list");
|
||||
req.setAttribute(SanitizeFilter.ATTR_STRIPPED_PATH, "/oapi/message/list");
|
||||
req.setAttribute(RoutingFilter.ATTR_ROUTE_SPEC, spec);
|
||||
if (xEntry != null) {
|
||||
req.addHeader(EntryDetector.HEADER_ENTRY, xEntry);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
/** 带合法三件套、且来自可信来源(标记为外部入口) */
|
||||
private MockHttpServletRequest signedExternalRequest(RouteSpec spec) {
|
||||
MockHttpServletRequest req = request(spec, EntryDetector.ENTRY_EXTERNAL);
|
||||
String ts = String.valueOf(System.currentTimeMillis());
|
||||
req.addHeader("X-App-Key", "app1");
|
||||
req.addHeader("X-Timestamp", ts);
|
||||
req.addHeader("X-Signature", SignUtil.signRequest("GET", "/oapi/message/list", ts, SECRET));
|
||||
return req;
|
||||
}
|
||||
|
||||
private MockHttpServletResponse run(SignVerifyFilter f, MockHttpServletRequest req, MockFilterChain chain)
|
||||
throws Exception {
|
||||
MockHttpServletResponse res = new MockHttpServletResponse();
|
||||
f.doFilter(req, res, chain);
|
||||
return res;
|
||||
}
|
||||
|
||||
@Test
|
||||
void A1_内部入口_ANON路由仍走验签不因策略放行() throws Exception {
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
// 无 X-Entry ⇒ 内部入口;即便路由是 EXTERNAL×ANON、限流就绪也不该免签放行
|
||||
MockHttpServletRequest req = request(EXTERNAL_ANON, null);
|
||||
MockHttpServletResponse res = run(filter(false, true, ALLOW_LIMITER), req, chain);
|
||||
|
||||
assertThat(res.getStatus()).as("内部入口不看 authMode:无签名即裸 401").isEqualTo(401);
|
||||
assertThat(req.getAttribute(SignVerifyFilter.ATTR_VERDICT)).isEqualTo(SignVerifyFilter.VERDICT_FAIL);
|
||||
assertThat(chain.getRequest()).as("链条中断").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void A1_内部入口_坏签名observe为true仍只记不拦() throws Exception {
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
MockHttpServletRequest req = request(EXTERNAL_ANON, null);
|
||||
MockHttpServletResponse res = run(filter(true, true, ALLOW_LIMITER), req, chain);
|
||||
|
||||
assertThat(res.getStatus()).as("内部入口的 observe 语义不变").isEqualTo(200);
|
||||
assertThat(req.getAttribute(SignVerifyFilter.ATTR_VERDICT)).isEqualTo(SignVerifyFilter.VERDICT_FAIL_OBSERVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void A2_外部入口打INTERNAL路由_裸403且不受observe影响() throws Exception {
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
// observe=true 也不放行:策略层拒绝与 observe 正交(同 400/404)
|
||||
MockHttpServletRequest req = request(INTERNAL_SIGNED, EntryDetector.ENTRY_EXTERNAL);
|
||||
MockHttpServletResponse res = run(filter(true, true, ALLOW_LIMITER), req, chain);
|
||||
|
||||
assertThat(res.getStatus()).isEqualTo(403);
|
||||
assertThat(res.getContentAsByteArray()).as("裸码无信封").isEmpty();
|
||||
assertThat(chain.getRequest()).isNull();
|
||||
assertThat(req.getAttribute(SignVerifyFilter.ATTR_VERDICT)).isEqualTo(SignVerifyFilter.VERDICT_FAIL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 外部入口_未经路由判定_同样裸403() throws Exception {
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
MockHttpServletRequest req = request(null, EntryDetector.ENTRY_EXTERNAL);
|
||||
MockHttpServletResponse res = run(filter(true, true, ALLOW_LIMITER), req, chain);
|
||||
|
||||
assertThat(res.getStatus()).as("spec 缺失按最严:外部入口永不默认放行").isEqualTo(403);
|
||||
assertThat(chain.getRequest()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void A3_外部入口EXTERNAL_SIGNED_无签名裸401() throws Exception {
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
MockHttpServletRequest req = request(EXTERNAL_SIGNED, EntryDetector.ENTRY_EXTERNAL);
|
||||
MockHttpServletResponse res = run(filter(false, true, ALLOW_LIMITER), req, chain);
|
||||
|
||||
assertThat(res.getStatus()).isEqualTo(401);
|
||||
assertThat(chain.getRequest()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 外部入口EXTERNAL_SIGNED_合法签名放行() throws Exception {
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
MockHttpServletRequest req = signedExternalRequest(EXTERNAL_SIGNED);
|
||||
MockHttpServletResponse res = run(filter(false, true, ALLOW_LIMITER), req, chain);
|
||||
|
||||
assertThat(res.getStatus()).isEqualTo(200);
|
||||
assertThat(chain.getRequest()).isNotNull();
|
||||
assertThat(req.getAttribute(SignVerifyFilter.ATTR_VERDICT)).isEqualTo(SignVerifyFilter.VERDICT_PASS);
|
||||
assertThat(req.getAttribute(SignVerifyFilter.ATTR_AUTH_APPKEY)).isEqualTo("app1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void A4_外部入口EXTERNAL_ANON_限流内免签放行且不注入身份头() throws Exception {
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
MockHttpServletRequest req = request(EXTERNAL_ANON, EntryDetector.ENTRY_EXTERNAL);
|
||||
MockHttpServletResponse res = run(filter(false, true, ALLOW_LIMITER), req, chain);
|
||||
|
||||
assertThat(res.getStatus()).isEqualTo(200);
|
||||
assertThat(chain.getRequest()).isNotNull();
|
||||
assertThat(req.getAttribute(SignVerifyFilter.ATTR_VERDICT)).as("台账 caller=anonymous 的依据")
|
||||
.isEqualTo(SignVerifyFilter.VERDICT_ANON);
|
||||
assertThat(req.getAttribute(SignVerifyFilter.ATTR_AUTH_APPKEY)).as("匿名就是匿名:不注入 appKey").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void A5_外部入口EXTERNAL_ANON_超限裸429() throws Exception {
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
MockHttpServletRequest req = request(EXTERNAL_ANON, EntryDetector.ENTRY_EXTERNAL);
|
||||
MockHttpServletResponse res = run(filter(false, true, EXCEED_LIMITER), req, chain);
|
||||
|
||||
assertThat(res.getStatus()).isEqualTo(429);
|
||||
assertThat(chain.getRequest()).isNull();
|
||||
assertThat(req.getAttribute(SignVerifyFilter.ATTR_VERDICT)).isEqualTo(SignVerifyFilter.VERDICT_FAIL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void A5_外部入口EXTERNAL_ANON_限流未就绪failClosd裸403() throws Exception {
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
MockHttpServletRequest req = request(EXTERNAL_ANON, EntryDetector.ENTRY_EXTERNAL);
|
||||
// 限流 bean 缺失(本期 UnavailableAnonRateLimiter 的等价形态)⇒ 不放行
|
||||
MockHttpServletResponse res = run(filter(false, true, null), req, chain);
|
||||
|
||||
assertThat(res.getStatus()).as("未接限流的匿名口子必须拒绝,不是放行").isEqualTo(403);
|
||||
assertThat(chain.getRequest()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void A6_来源不可信时伪造XEntry无效_按内部入口处理() throws Exception {
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
// trusted-proxies 为空(来源不可信)——客户端自带 X-Entry: external 也不能自封外部入口
|
||||
MockHttpServletRequest req = request(EXTERNAL_ANON, EntryDetector.ENTRY_EXTERNAL);
|
||||
MockHttpServletResponse res = run(filter(false, false, ALLOW_LIMITER), req, chain);
|
||||
|
||||
assertThat(res.getStatus()).as("伪造标记无效:走内部口径验签 → 401").isEqualTo(401);
|
||||
assertThat(chain.getRequest()).isNull();
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,9 @@ import java.util.concurrent.Executors;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* StreamingForwarder 五用例(T1.3 TDD,假后端 = JDK 自带 com.sun.net.httpserver.HttpServer,
|
||||
* StreamingForwarder 用例(T1.3 TDD,假后端 = JDK 自带 com.sun.net.httpserver.HttpServer,
|
||||
* 零新增测试依赖):GET 转发 forwardUrl 逐字符 + 响应透传;POST body 流式透传;
|
||||
* 客户端伪造 X-Caller-App-Key 不出现在转发请求;签名头四件原样到达;
|
||||
* 客户端伪造 X-Caller-App-Key / X-Entry 不出现在转发请求;签名头四件原样到达;
|
||||
* 超时裸 504 / 连接拒绝裸 502(另附 body 超限裸 413——实现要点契约)。
|
||||
*/
|
||||
class StreamingForwarderTest {
|
||||
@@ -169,6 +169,19 @@ class StreamingForwarderTest {
|
||||
assertThat(lastHeaders).as("身份头唯一合法来源是网关(T1.4)").doesNotContainKey("x-caller-app-key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 入口标记头XEntry不透传提供方() throws Exception {
|
||||
MockHttpServletRequest req = request("GET", "/userbase/oapi/x");
|
||||
req.addHeader("X-Entry", "external");
|
||||
MockHttpServletResponse res = new MockHttpServletResponse();
|
||||
|
||||
forwarder().forward(req, res, "http://127.0.0.1:" + port + "/userbase/oapi/x", 5000, 1024 * 1024);
|
||||
|
||||
assertThat(res.getStatus()).isEqualTo(201);
|
||||
assertThat(lastHeaders).as("T4.3:入口标记是网关与 Caddy 之间的约定,提供方不该看到(伪造值也到不了)")
|
||||
.doesNotContainKey("x-entry");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 签名头四件原样到达假后端() throws Exception {
|
||||
MockHttpServletRequest req = request("GET", "/userbase/oapi/x");
|
||||
|
||||
@@ -21,3 +21,8 @@ management:
|
||||
# 测试上下文没有 redis(默认 localhost:6379 连不上会让聚合探针 503);
|
||||
# E04a 用例要钉的是「探针不经 oapi 链」= 平台探活口径 HTTP 200
|
||||
enabled: false
|
||||
|
||||
yh:
|
||||
gateway:
|
||||
# 变更广播需 Redis:测试上下文关掉(路由判定读本地 Caffeine,与广播无关)
|
||||
redis-broadcast-enabled: false
|
||||
|
||||
Reference in New Issue
Block a user