Gateway 路由:谓词、过滤器顺序与头处理
本页回答一个具体问题:桌面客户端发送 GET /edge/orders/42?view=brief 后,哪个路由被选中,真正发往哪里,路径、参数与头经过哪些修改,失败时怎样定位。可靠性、安全身份、限流、body 与协议在下一页承接。
版本、产品形态与最小工程
U32(#69)对应 PRD L02。固定 Spring Cloud Gateway 5.0.4,commit 2780fa2117ce3f8af662aa3d485ed77743aa6fb0;Boot 4.0.9 + Cloud 2025.1.4,本批实际运行为 Oracle JDK 21.0.5、Maven 3.9.9、macOS 26.7.1 x86_64(候选基线的 Liberica 21.0.11/aarch64 未在本机复现)。本文配置与源码都属于 Server WebFlux,不能混入 Server MVC 的函数路由或 Proxy Exchange 的控制器参数。版本支持和环境解释回查工程基线。本页是源码核查与可执行配置说明;配套的真实 HTTP 观察在 labs/cloud-gateway/webflux 与本批证据中,未运行的分支按节标注,不把 R0 或官方测试审读称为本实验通过。
| 5.0.4 产品形态 | 依赖/运行与实际出站客户端 | 本轮去向 |
|---|---|---|
| Server WebFlux | spring-cloud-starter-gateway-server-webflux;WebFlux、Reactor Netty;NettyRoutingFilter | 本页与下一页完整承担;不能部署传统 Servlet WAR |
| Server Web MVC | spring-cloud-starter-gateway-server-webmvc;Servlet/WebMvc.fn;默认 RestClientProxyExchange | 由 MVC 作者承担 G15;不能套本页链顺序、Redis 与 Netty 参数 |
| Proxy Exchange WebFlux | spring-cloud-gateway-proxyexchange-webflux;注解 handler 参数,WebClient | 由 Proxy 作者承担 G16;不自动继承 Server 路由工厂与 Actuator |
| Proxy Exchange MVC | spring-cloud-gateway-proxyexchange-webmvc;注解 handler 参数,仍为 RestTemplate | G16 独立工程;不能因 Server MVC 用 RestClient 就改称它也用 RestClient |
这里的 Route 是已装配的执行对象,含 id、目标 URI、order、predicate、filters、metadata;RouteDefinition 是配置输入,尚须工厂解析。Predicate 选择路由,GatewayFilter 改请求/响应,GlobalFilter 参加每个已选路由的链,HttpHeadersFilter 在代理出入站处理协议头。HTTP 认证 WebFilter 是更外层的另一条链。固定官方术语和四形态入口给出职责边界。
最小 pom.xml 如下;选 lb://orders 时再加 spring-cloud-starter-loadbalancer。不要同时加入两个 Server starter。这里展示最小依赖组合;可运行的完整工程在 labs/cloud-gateway,四个形态各自独立 POM,不共用 classpath。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.9</version><relativePath/>
</parent>
<groupId>study</groupId><artifactId>gateway-lab</artifactId><version>1.0.0</version>
<properties><java.version>21</java.version></properties>
<dependencyManagement><dependencies><dependency>
<groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId>
<version>2025.1.4</version><type>pom</type><scope>import</scope>
</dependency></dependencies></dependencyManagement>
<dependencies>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway-server-webflux</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>package study.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}本页所有 YAML 片段都放在此完整基线下;后面的 predicates 或 filters 替换同一路由对应列表,不是把每段互相 AND/串接。约定受控 echo 上游监听 127.0.0.1:19081,网关监听 127.0.0.1:19080;它们是教学示例端口,主任务 runner 应实际分配空闲端口并记录。echo 需要返回收到的 raw path/query、Host、合成头与请求编号,单凭客户端 200 不能证明转换正确。
server:
address: 127.0.0.1
port: 19080
spring:
cloud:
gateway:
server:
webflux:
fail-on-route-definition-error: true
route-filter-cache-enabled: false
discovery:
locator:
enabled: false
routes:
- id: orders-r1
uri: http://127.0.0.1:19081
order: -10
metadata:
release: r1
connect-timeout: 200
response-timeout: 700
predicates:
- Path=/edge/orders/**
- Method=GET
filters:
- StripPrefix=1
- AddRequestHeader=X-Lab-Route,orders-r1mvn spring-boot:run 启动上述应用;在 echo 已启动且端口匹配时执行 curl -i 'http://127.0.0.1:19080/edge/orders/42?view=brief',预期 echo 看到 /orders/42?view=brief 与 X-Lab-Route。没有 echo 会进入连接失败分支;这不是谓词未匹配。这段 YAML 是手工复现步骤,本次没有单独启动 19080 端口;等价路径改写已由实验工程的 /api/items/7 真实运行覆盖,见本节末尾的运行边界。
原码连续摘录:GatewayProperties.java,5.0.4,L45–L80;整文件 SHA-256 667deeb6984024a862243bf57038dbc2398e5b5f465ff609d0d41d88b53c80ec。
public static final String PREFIX = "spring.cloud.gateway.server.webflux";
private final Log logger = LogFactory.getLog(getClass());
/**
* List of Routes.
*/
@NotNull
@Valid
private List<RouteDefinition> routes = new ArrayList<>();
/**
* List of filter definitions that are applied to every route.
*/
private List<FilterDefinition> defaultFilters = new ArrayList<>();
private List<MediaType> streamingMediaTypes = Arrays.asList(MediaType.TEXT_EVENT_STREAM,
new MediaType("application", "stream+json"), new MediaType("application", "grpc"),
new MediaType("application", "grpc+protobuf"), new MediaType("application", "grpc+json"));
/**
* Option to fail on route definition errors, defaults to true. Otherwise, a warning
* is logged.
*/
private boolean failOnRouteDefinitionError = true;
/**
* Enables the route filter cache, defaults to false.
*/
private boolean routeFilterCacheEnabled = false;
/**
* Regular expression defining proxies that are trusted when they appear in a
* Forwarded or X-Forwarded header.
*/
private String trustedProxies;源码中的 PREFIX 是 spring.cloud.gateway.server.webflux;本页已将官方旧样例残留的 spring.cloud.gateway.routes/default-filters/httpclient 改成此版本真实绑定前缀。默认 failOnRouteDefinitionError=true;routeFilterCacheEnabled=false 只关闭合并 filter 列表缓存,不关闭路由缓存。
路由从定义到发布:缓存与原子性边界
一次转发按下列链路理解:
flowchart LR
D[Properties / Java DSL / Discovery / Repository] --> C[CachingRouteLocator 排序及缓存]
C --> M[RoutePredicateHandlerMapping 首个匹配]
M --> F[FilteringWebHandler 合并过滤器]
F --> U[RouteToRequestUrl / 可选 LoadBalancer]
U --> N[NettyRoutingFilter 或其他 scheme 路由器]
N --> W[NettyWriteResponseFilter 写响应]Properties 提供 RouteDefinition 列表;Java DSL 直接生成 RouteLocator。两者不要求互斥,但不要给同一业务重复定义相同 id。下例替代 YAML 中同名路由,说明 AND、OR、negate 的作用域:
@Bean
RouteLocator routes(RouteLocatorBuilder b) {
return b.routes()
.route("orders-java", r -> r.order(-20)
.path("/edge/orders/**").and().method("GET")
.filters(f -> f.stripPrefix(1).addRequestHeader("X-Lab-Route", "java"))
.uri("http://127.0.0.1:19081"))
.route("help", r -> r.path("/help/**").or().host("help.example.test")
.uri("http://127.0.0.1:19081"))
.route("other-methods", r -> r.method("GET").negate()
.uri("http://127.0.0.1:19081"))
.build();
}以上 bean 置于 @Configuration 类,导入 Bean、RouteLocator、RouteLocatorBuilder。最后一条只是演示取反,真实入口应再加 Path;没有路径限制会接收几乎全部非 GET 请求。order 小者先评估,.next() 取第一个通过的 route,后面更“具体”的 Path 不会自动夺回请求;同 order 不应作为发布优先级合同。同步调用 predicate 抛出的异常可被当前 catch 记日志并转 false;返回的异步 Publisher 发错不被这个同步 catch 自动吞掉。
原码连续摘录:RoutePredicateHandlerMapping.java,5.0.4,L131–L164;整文件 SHA-256 46ea93e38116cb3116fb274161aa7b65f78633716a6de85b72ba4bf7c5d52859。
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes().filterWhen(route -> {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, route.getId());
try {
return route.getPredicate().apply(exchange);
}
catch (Exception e) {
logger.error("Error applying predicate for route: " + route.getId(), e);
}
return Mono.just(false);
})
.next()
// TODO: error handling
.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("Route matched: " + route.getId());
}
validateRoute(route, exchange);
return route;
});
/*
* TODO: trace logging if (logger.isTraceEnabled()) {
* logger.trace("RouteDefinition did not match: " + routeDefinition.getId()); }
*/
}
/**
* Validate the given handler against the current request.
* <p>
* The default implementation is empty. Can be overridden in subclasses, for example
* to enforce specific preconditions expressed in URL mappings.
* @param route the Route object to validateDiscovery locator 默认关闭。有意启用且已引入发现与 LB 时,可在 spring.cloud.gateway.server.webflux.discovery.locator 配置 enabled: true、include-expression: "serviceId == 'orders'";默认生成 lb://serviceId、Path=/serviceId/** 及去掉 serviceId 前缀的 RewritePath。不要误以为发现服务目录就代表允许公开全部服务。自定义 predicates/filters 会替代默认列表,须显式保留需要的 Path 与重写。静态 SimpleDiscoveryClient 只提供输入目录,没有注册中心租约或真实摘流协议;服务发现的核心由 L01 作者承担。固定Discovery locator可逐项对照表达式。
共享路由可加 reactive Redis starter 并设 spring.cloud.gateway.server.webflux.redis-route-definition-repository.enabled: true;RedisRouteDefinitionRepository 保存的是定义,仍要经过本机装配、缓存与 refresh。默认内存仓库重启不耐久。管理写入、删除、批量定义以及 POST /actuator/gateway/refresh 的权限在可靠性页;“保存成功”与“所有网关已经执行新版”是两项证据。参考固定共享路由。
要区分四种缓存:route locator 缓存执行路由快照;route-filter-cache 缓存某 route 的合并 filter 列表;LB 缓存实例目录;LocalResponseCache 缓存业务响应。删除响应缓存不能更新路由,refresh 路由也不代表清空所有响应或旧实例。
固定 CachingRouteLocator 的全量事件分支先 fetch().collect(...),成功后再替换本机 routes 项;失败发 RefreshRoutesResultEvent 并保留已有项。范围刷新不同:指定 metadata 的 fetch 错误可经 onErrorResume 变成空列表,再与不在范围内的旧路由合并;不能笼统承诺“一切刷新失败都保留旧路由”。直接调用 refresh() 又是先删缓存再懒读取,不等同事件分支。在途 exchange 已持有的 Route 不会被全局追溯替换;两个并发 refresh、仓库多次写、多个网关都不构成同一事务。
原码连续摘录:CachingRouteLocator.java,L92–L135;整文件 SHA-256 5dbd9fd3b3eda599ddd3fcaa090fb15c5d1f79c57be1a36f4a17b912d1d02ce4。
scopedRoutes.subscribe(scopedRoutesList -> {
updateCache(Flux.concat(Flux.fromIterable(scopedRoutesList), getNonScopedRoutes(event))
.sort(AnnotationAwareOrderComparator.INSTANCE));
}, this::handleRefreshError);
}
else {
final Mono<List<Route>> allRoutes = fetch().collect(Collectors.toList());
allRoutes.subscribe(list -> updateCache(Flux.fromIterable(list)), this::handleRefreshError);
}
}
catch (Throwable e) {
handleRefreshError(e);
}
}
private synchronized void updateCache(Flux<Route> routes) {
routes.materialize()
.collect(Collectors.toList())
.subscribe(this::publishRefreshEvent, this::handleRefreshError);
}
private void publishRefreshEvent(List<Signal<Route>> signals) {
cache.put(CACHE_KEY, signals);
Objects.requireNonNull(applicationEventPublisher, "ApplicationEventPublisher is required");
applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this));
}
private Flux<Route> getNonScopedRoutes(RefreshRoutesEvent scopedEvent) {
return this.getRoutes()
.filter(route -> !RouteLocator.matchMetadata(route.getMetadata(), scopedEvent.getMetadata()));
}
private void handleRefreshError(Throwable throwable) {
if (log.isErrorEnabled()) {
log.error("Refresh routes error !!!", throwable);
}
Objects.requireNonNull(applicationEventPublisher, "ApplicationEventPublisher is required");
applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this, throwable));
}
@Override
public int getOrder() {
return 0;发布诊断顺序是定义版本 → 解析错误 → 本机 refresh 结果 → 本机可见 route id/metadata → echo 的实际目标。非法 factory 名、无效 regex、绑定值错误先修配置;未启用 fail-fast 时警告并跳过坏定义会造成部分路由缺失,不应将“进程仍活着”当发布成功。恢复应重新发布最后一个已验证版本,再逐节点核对;不能靠反复刷新期望自愈。
谓词选择:时间、内容、地址与版本
以下各项的公共输入是基线路由的一次 exchange,输出是是否参加该 route,不修改业务身份。每段中的列表独立替换 predicates。AND 条件中的任一项失败都不会运行该 route 的 filters;诊断时记录实际 method、raw path、时区、头与匹配 route,而不是只看结果码。固定全部谓词文档及子节包含下述 15 项;CloudFoundry 源码补项另述。
时间族用于发布窗口,比较网关时钟,不能用客户端自报时间决定权限:
# 三个独立实验分别选一项
predicates:
- 'After=2026-10-26T20:00:00+08:00[Asia/Shanghai]'
# - 'Before=2026-10-26T21:00:00+08:00[Asia/Shanghai]'
# - 'Between=2026-10-26T20:00:00+08:00[Asia/Shanghai],2026-10-26T21:00:00+08:00[Asia/Shanghai]'After 在指定瞬间之后才通过,恰好相等不通过;Before 在之前通过,相等不通过;Between 需开始早于结束,使用开区间,两端均不通过。写无偏移时间或反转区间会让解析/校验失败。实验用受控 Clock/边界测试或把窗口移到当前时间前后;不要修改宿主机时钟。跨时区灰度应统一 Instant,时钟偏差要列入发布窗口成本。
内容族适合选择协议变体,不能承担认证:
predicates:
- 'Cookie=release,blue-[0-9]+'
- 'Header=X-Lab-Version,v[0-9]+'
- 'Query=view,brief|full'Cookie 需要同名 cookie 的某个值完整匹配 Java regex;release=blue-12 通过,缺 cookie、blue-x 不通过。Header 示例匹配 v2,prefix-v2 不通过;只写 Header=X-Flag 是存在性检查,空值与合法授权不是一回事。Query 可只写 Query=view 检查存在,或带 regex 匹配任一同名值;?view=full 通过,缺参数不通过,存在但空值要按所用 regex 判断。用单引号保护 regex 的反斜杠;不写灾难性回溯正则处理无界头值,配合本页大小上限。多值头/参数必须测试重复值,不能只检查客户端打印的首值。
Host、Method、Path 决定入口资源形状:
predicates:
- Host={tenant}.example.test,api.example.test
- Method=GET,HEAD
- name: Path
args:
patterns: /edge/orders/{id},/edge/health
matchTrailingSlash: falseHost 使用以 . 分隔的 Ant 风格模式,a.example.test 可提取 tenant;不是正则 .*。Path 使用路径模式,/edge/orders/42 提取 id,多个 patterns 在该工厂内部是 OR;它们再与 Host/Method AND。Method 是列出方法的集合,GET 不自动意味着 POST,也不要假设显式 Method=GET 会接纳 HEAD。Host 端口、大小写/规范化、编码路径要看实际解析值,不能拿 Host 变量直接当可信租户。
matchTrailingSlash 在此 Gateway 5.0.4 工厂默认 true,与从 Framework 某个默认值类推不同;上例显式 false 后 /edge/orders/42/ 不通过。本页选择显式值保证升级可复验。/edge/orders/a%2Fb 的 raw path、路由提取、上游解码可能各不相同,不应靠替换字符串绕过安全层;用 curl --path-as-is 和 echo 同时观察。
原码连续摘录:PathRoutePredicateFactory.java,5.0.4,L158–L165;整文件 SHA-256 3011efb9dd2bd8cddc22d886044548c2b9e0199a0d85e9bb6eefe97dc8b6b4a3。
private boolean matchTrailingSlash = true;
public List<String> getPatterns() {
return patterns;
}
public Config setPatterns(List<String> patterns) {
this.patterns = patterns;地址族先决定“地址来自哪一层”:
predicates:
- RemoteAddr=127.0.0.1/32,::1/128
# 独立对照:使用转发头的地址,不自动成为可信名单
# - XForwardedRemoteAddr=192.0.2.0/24RemoteAddr 默认针对远端连接地址,例中本机 peer 通过;RemoteAddr 接受 CIDR,后文 trusted-proxies 则接受正则,二者不能互换。要读取可信代理后原始地址,Java DSL 可用 .remoteAddr(XForwardedRemoteAddressResolver.maxTrustedIndex(1), "192.0.2.0/24"),导入 org.springframework.cloud.gateway.support.ipresolver.XForwardedRemoteAddressResolver;这只按可信跳数从右侧取,不校验这一跳实际来自可信网段。trustAll() 取最左地址,外部可伪造。XForwardedRemoteAddr 工厂本身依赖 XFF,不能将该谓词充当代理身份验证。连接访问控制与头清洗必须先成立,见后文。
Weight 用于同一组多个 route 的概率分配:
routes:
- id: stable
uri: http://127.0.0.1:19081
predicates: [Path=/edge/orders/**, 'Weight=orders,8']
- id: canary
uri: http://127.0.0.1:19082
predicates: [Path=/edge/orders/**, 'Weight=orders,2']在同组、候选条件一致且样本足够时趋向 80%/20%,十次请求不保证八二;额外给 canary 加互斥条件会使选中它的请求落空。它不探测健康,不是每租户粘滞,也不按实际资源成本均分。改变权重后需重新形成路由快照,不能把上游下线当权重自动归零;架构取舍见“路由发布与边缘转换的容量选择”。
Version 先使用 Framework 的策略解析请求,再比较版本:
spring:
webflux:
apiversion:
use:
header: X-API-Version
required: true
cloud:
gateway:
server:
webflux:
routes:
- id: versioned
uri: http://127.0.0.1:19081
predicates: [Path=/edge/orders/**, 'Version=1.3']请求 X-API-Version: 1.3 可通过固定版本;Version=1.1+ 是 baseline,接受解析结果大于等于 1.1 的版本。解析/支持版本验证由策略裁决,不等于任意字符串可比较;可改用 use.query-parameter: apiVersion、use.media-type-parameter: {"[application/json]": version} 或 use.path-segment(零基索引),这些 resolver 需明确优先级与冲突输入,不把它们与 L01 的 LB 实例 API version 匹配混教。
源码有一个重要条件:无 ApiVersionStrategy 时,首次解析 route 版本会先触发 Assert.state;官方此提交文档的“未配置策略总通过”说明不完整。策略存在、请求版本为空时,才会在后面的分支返回 true;若业务要求每次带版本,应设 required: true,另测缺值、非法值、不支持版本与恢复。不能将缺策略引起的匹配异常称为安全降级。
原码连续摘录:VersionRoutePredicateFactory.java,L91–L113;整文件 SHA-256 42d200ad4ef9aa8764ce920001bd1efa0dfc048e779df10e7b07cea78514ec08。
}
int result = compareVersions(config.parsedVersion, requestVersion.getVersion());
boolean match = (config.baselineVersion ? result <= 0 : result == 0);
traceMatch("Version", config.version, requestVersion, match);
return match;
}
private <V extends Comparable<V>> int compareVersions(Object v1, Object v2) {
return ((V) v1).compareTo((V) v2);
}
@Override
public Object getConfig() {
return config;
}
@Override
public String toString() {
return String.format("Version: %s", config.version + (config.baselineVersion ? "+" : ""));
}
};
}ReadBody 只能 Java DSL 配置 lambda,不能在 YAML 里塞 Java 表达式:
@Bean
RouteLocator bodyRoute(RouteLocatorBuilder b) {
return b.routes().route("body-choice", r -> r.path("/body-choice")
.and().readBody(String.class, body -> body.contains("yes"))
.uri("http://127.0.0.1:19081")).build();
}文本 yes please 通过,no 不通过;body 解码失败、空体与大小超限是独立反例,不能假定都返回 false。第一次读取需要缓存/重放,多个 route 不能用不同类型随意反复解码同一流;资源、取消与大 body 选择由body 章节承担。
CloudFoundryRouteService 是目录外源码补项:predicates: [{name: CloudFoundryRouteService}] 检查 X-CF-Forwarded-Url、X-CF-Proxy-Signature、X-CF-Proxy-Metadata 三头存在;少一头失败,三头任意合成值可通过存在性匹配。该工厂不验签,不构成可信平台身份。固定源码 L61–L72解释其 AND;平台签名、重放与真实路由服务协议由 U35 负责,本页不将检查头存在计为平台集成通过。
过滤链:合并排序、嵌套退出与自定义扩展
选中 route 后,FilteringWebHandler 把 GlobalFilter 适配成 GatewayFilter,与 route filters 合并,按 Ordered 排序。数值越小越早执行 pre,正常完成的 post 通常按嵌套反向退出。GlobalFilter 不是整段先执行完再交给 route filters。
原码连续摘录:FilteringWebHandler.java,5.0.4,L119–L127;整文件 SHA-256 c86d87a1c6fe9b3d66157419bda1b974da62f7404a64c2fa93e76d073410395e。
protected List<GatewayFilter> getAllFilters(Route route) {
List<GatewayFilter> gatewayFilters = route.getFilters();
List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
combined.addAll(gatewayFilters);
AnnotationAwareOrderComparator.sort(combined);
return combined;
}
private static class DefaultGatewayFilterChain implements GatewayFilterChain {普通未实现 Ordered 的 default-filters 与 route filters 分别从 1 编号,稳定合并后,同位置 default 在前。默认 A/B/C/D,route X/Y,pre 是 A X B Y C D,正常 .then(...) post 为反向。自带 Ordered 的工厂保留自身 order,因此配置列表位置不是最终顺序的完整证据。下面是可执行的独立示例:
default-filters:
- AddRequestHeader=X-Flow,A
- AddRequestHeader=X-Flow,B
routes:
- id: order-lab
uri: http://127.0.0.1:19081
predicates: [Path=/order]
filters:
- AddRequestHeader=X-Flow,X
- AddRequestHeader=X-Flow,Yecho 预期收到 X-Flow 的四个值 A/X/B/Y,按真实多值头记录;要验证 post 则需显式日志,不能从响应头多值顺序推所有完成/取消行为。短路返回 setComplete 时后续链不进入,.then(post) 只在正常完成触发;错误处理用 doOnError/onErrorResume,释放用 doFinally,并区分 CANCEL、ON_ERROR、ON_COMPLETE。响应已提交后不能再写状态或第二份 body。
自定义 predicate/filter 是明确的公共扩展点。下列类分别放到应用扫描包,各自为独立 Java 文件;它们演示可绑定参数与边界,不把头存在当认证:
@Component
public class LabFlagRoutePredicateFactory
extends AbstractRoutePredicateFactory<LabFlagRoutePredicateFactory.Config> {
public LabFlagRoutePredicateFactory() { super(Config.class); }
@Override public List<String> shortcutFieldOrder() { return List.of("value"); }
@Override public Predicate<ServerWebExchange> apply(Config c) {
return e -> c.getValue().equals(e.getRequest().getHeaders().getFirst("X-Lab-Flag"));
}
public static class Config {
private String value = "on";
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
}@Component
public class LabStampGatewayFilterFactory
extends AbstractGatewayFilterFactory<LabStampGatewayFilterFactory.Config> {
public LabStampGatewayFilterFactory() { super(Config.class); }
@Override public List<String> shortcutFieldOrder() { return List.of("value"); }
@Override public GatewayFilter apply(Config c) {
return (exchange, chain) -> chain.filter(exchange.mutate().request(
exchange.getRequest().mutate().headers(h -> h.set("X-Lab-Stamp", c.getValue())).build()
).build());
}
public static class Config {
private String value = "r1";
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
}分别导入 Component、List、Predicate、ServerWebExchange、对应 Abstract 工厂与 GatewayFilter;配置 predicates: [Path=/custom, 'LabFlag=on']、filters: ['LabStamp=r2']。入站 X-Lab-Flag=on 时,上游 X-Lab-Stamp 从任何旧值覆盖成 r2;缺 flag 不匹配,错误工厂名导致定义装配失败。工厂类名以 RoutePredicateFactory/GatewayFilterFactory 结尾,配置用去后缀的名称;Config 是每份工厂配置,不是并发请求的可变存储。
自定义全局 filter 则实现 GlobalFilter 与 Ordered;例如独立 bean:
@Bean
GlobalFilter markIngress() {
return new MarkIngress();
}
static class MarkIngress implements GlobalFilter, Ordered {
@Override public int getOrder() { return -50; }
@Override public Mono<Void> filter(ServerWebExchange e, GatewayFilterChain chain) {
long started = System.nanoTime();
return chain.filter(e).doFinally(signal ->
System.out.println("lab signal=" + signal + " elapsedNs=" + (System.nanoTime() - started)));
}
}导入 GlobalFilter、GatewayFilterChain、Ordered、Mono。此处 System.out 仅合成实验;生产使用受控日志/指标。回调非阻塞、不保存 exchange 到单例字段、不可自行 subscribe;否则订阅上下文、取消和资源责任会断开。自定义 header filter 另实现 HttpHeadersFilter 的 REQUEST/RESPONSE 类型,不用 GlobalFilter order 推断它的协议执行顺序。固定开发指南和default 顺序是配置名及扩展合同。
路径与目标 URI:观察最终发出的请求
考虑 GET /edge/orders/42?view=brief。原始 URL、route 的 uri、exchange 的 GATEWAY_REQUEST_URL_ATTR 与最终物理 URI 是不同状态:RouteToRequestUrlFilter 组合目标 scheme/host 与当前请求路径;lb://orders 再由 ReactiveLoadBalancerClientFilter 选实例并重建物理 scheme/host/port。GATEWAY_ORIGINAL_REQUEST_URL_ATTR 保留历史 URL;scheme prefix 可表示 lb:ws://orders;gatewayAlreadyRouted 防止重复代理。自定义组件跳过这些标记可能双发或循环,不能靠“看到 route URI”就证明发送成功。
对同一输入,以下独立配置给出不同合法合同:
filters:
- StripPrefix=1
- PrefixPath=/v1
# 最终 /v1/orders/42?view=briefStripPrefix 的参数是路径段数,不是字符数;/a/b/c 配 2 变 /c,短路径/连续斜线要回显验证。PrefixPath 给当前路径前加固定前缀,重复执行会重复加;把它放在 Retry 后需验证重入。二者都不自动改认证路径策略,也不把 query 当路径处理。
predicates: [Path=/edge/orders/{id}]
filters:
- SetPath=/v2/items/{id}
# /edge/orders/42 -> /v2/items/42SetPath 用已匹配 URI 变量替换整个 path,缺变量是配置/运行输入错误;它不是正则。若要保留任意剩余段,用 RewritePath:
filters:
- 'RewritePath=/edge/(?<rest>.*), /v2/$\{rest}'/edge/orders/42 变 /v2/orders/42;$\{rest} 是 YAML/Spring 配置里的替换转义,不写成会被外部属性解析的裸 ${rest}。未匹配 regex 保持相应路径,非法 regex 在工厂构造时报错。不要对 raw URL 全串 replace;百分号编码、%2F、非 ASCII、重复斜线与 query 必须分别观察。签名请求重写 path 会影响签名,责任由入口与上游协议共同约定。
SetRequestUri 与 RequestHeaderToRequestUri 改目标,风险高于改 path:
# 独立路由,不能把任意客户端输入带进目标主机
uri: no://op
predicates: [Path=/fixed/**]
filters:
- SetRequestUri=http://127.0.0.1:19081输入 /fixed/42 的目标 authority 改为示例 echo,原请求路径仍参与最终组合;SetRequestUri=http://{appId}.example.test 可以展开 Path/Host 的 appId,必须先做固定映射/允许名单,不能把任意域名片段当服务发现。相对/非法 URI 不能作为有效绝对目标,源码分支会保留原路由地址;no://op 因而不会神奇回退为可调用上游。工厂名是 SetRequestUri,官方文件名虽叫 seturi,配置不是 SetUri。
uri: http://127.0.0.1:19081
filters:
- RequestHeaderToRequestUri=X-Internal-Target若可信内部组件写 X-Internal-Target: http://127.0.0.1:19082/selected,它覆盖请求目标;缺头/非法 URI 保持原目标。完整路径如何组合要以固定 AbstractChangeRequestUriGatewayFilterFactory 后的最终属性和 echo 为准,不能从头名推断“只改 host”。入口必须在更早的可信边界删除外部同名头、只允许内部路由层重建,并限定 scheme/host/port;单用这个工厂没有 SSRF 防护。固定目标覆盖文档也要求可信环境。
头与参数转换:追加、覆盖、删除和多值语义
本节是在同一完整链路上调整 HTTP 合同:上游 echo 记录入站变化,下游响应夹具提供重复头供网关改写,客户端记录最终值。每条可独立放在基线 filters;URI 模板变量仅来自本次已经匹配的 Path/Host,不能把变量当可信身份。成本与恢复映射到“路由发布与边缘转换的容量选择”中的头合同案例。
请求头先分清四种意图:
filters:
- AddRequestHeader=X-Feature,blue
- AddRequestHeadersIfNotPresent=X-Default:brief,X-Other:yes
- SetRequestHeader=X-Contract,v2
- RemoveRequestHeader=X-Untrusted-User
- MapRequestHeader=X-Legacy-Trace,X-Lab-TraceAddRequestHeader 对已有 X-Feature:red 追加 blue,形成多值;不是覆盖。AddRequestHeadersIfNotPresent 仅在头缺失时补默认,已有 X-Default=full 会保留 full,因此不适合覆盖外来安全身份;同名多项可生成多值。SetRequestHeader 以 v2 替换该名全部旧值,变量例 X-Order,{id} 要有匹配 Path。RemoveRequestHeader 不存在时是无变化,删除操作在 route 链内发生,不能取代更外层认证前的清洗。MapRequestHeader 把源值加到目标,目标已有值会追加,源头仍保留;源缺失无变化,不能用它实现“改名后删除”。以上顺序会改变最终多值,验证要提交重复头,不能只提交单值正常例。
Query 的合同独立于 body 和身份:
filters:
- AddRequestParameter=source,edge
- RewriteRequestParameter=view,brief
- RemoveRequestParameter=debug?view=full&view=raw&debug=1 变成单一 view=brief,新增 source=edge,移除所有 debug。AddRequestParameter 已有 source 时追加,支持 URI 变量;RewriteRequestParameter 只在参数已存在时改成单值,缺参数不会新增,参数 replacement 不是 regex;RemoveRequestParameter 缺值无变化,不删 JSON body 里的同名字段。签名 query、排序、空值和重复参数须与上游解析保持一致,不能仅比较浏览器展示的 URL。
响应头的动作与请求头方向相反:
filters:
- AddResponseHeader=X-Lab,edge
- SetResponseHeader=X-Contract,v2
- RemoveResponseHeader=X-Internal-Node
- 'RewriteResponseHeader=X-Example,password=[^&]+,password=***'
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials,RETAIN_UNIQUEAddResponseHeader 给上游 X-Lab 原值追加 edge;SetResponseHeader 覆盖全部旧值;RemoveResponseHeader 去掉内部节点头,对没有该头的响应无变化。RewriteResponseHeader 在值中执行 regex 替换,例如 a=1&password=secret&b=2 变成遮蔽值;它不修改 body,不保证所有秘密都被脱敏,日志应另行治理。DedupeResponseHeader 对指定头去重,RETAIN_FIRST 默认留首值,RETAIN_LAST 留末值,RETAIN_UNIQUE 留唯一值;不同 origin 仍是不同值,不能借“去重”修复互相冲突的 CORS 策略,Set-Cookie 多值也不能随意合并。
Host 有两个独立控制:PreserveHostHeader(无参数)令代理出站保留入站 Host;SetRequestHostHeader=orders.internal.test 显式设置出站 Host。没有 Preserve 时 Netty 通常按目标 URI 生成 Host;不要用 Preserve 透传未验证的公网 Host 到虚拟主机/重定向逻辑。两个工厂组合需观察最终 Host,并且 Host 不等于 TLS SNI/证书验证对象。请求改成 https 上游时,仅改 Host 不会把不匹配证书变合法。
这些具体工厂均有固定5.0.4 目录;目录只用于查阅,本节已给配置、输入、输出和重复/缺失分支,不以名字出现代替行为讲解。
响应重定向、状态与大小边界
RewriteLocationResponseHeader 解决“上游返回内部地址”的合同问题:
filters:
- name: RewriteLocationResponseHeader
args:
stripVersionMode: AS_IN_REQUEST
locationHeaderName: Location
hostValue: api.example.test
protocolsRegex: https?|ftps?请求外部 /orders,上游 Location=https://orders.internal/v2/orders/42 时,可改外部 host 并按 AS_IN_REQUEST 去掉版本段;请求自己带版本时保留。NEVER_STRIP 从不去版本,ALWAYS_STRIP 总是去;默认 hostValue 未设会取请求 Host,所以入口应验证 Host。协议不匹配 regex 或无 Location 时不改。它不改状态、body 中链接或所有任意相对 URL,不能当通用页面代理。回显 Location 后再跟随验证,不默认允许客户端跟随到外部未知地址。
# 以下为三个独立实验
filters:
- 'RedirectTo=302,https://api.example.test/help,true'
# filters: [SetStatus=BAD_GATEWAY]
# filters: [SetStatus=401]RedirectTo 直接生成带 Location 的 3xx,第三参数 includeRequestParams=true 将原 query 带过去;false 为默认,不转发上游,URL 有逗号时用展开 args。302 与 307/308 的方法保留合同不同,重定向目标必须固定/验证。SetStatus 在已有链上改状态,接受数字或枚举,不自动替换上游 body,也不自动拒绝执行上游;401 不等于完成了认证挑战。要保留原状态可设 spring.cloud.gateway.server.webflux.set-status.original-status-header-name: X-Original-Status,不应依赖改码让失败统计消失。
filters:
- name: RequestSize
args:
maxSize: 1MB
- name: RequestHeaderSize
args:
maxSize: 8KB
errorHeaderName: X-Lab-Header-ErrorRequestSize 在 5.0.4 读取声明 Content-Length,大于上限返回 413,默认上限 5,000,000 B;没有 Content-Length 的 chunked 流不由此工厂逐字节累计,因此它不能独立防止大 body 缓存。应另设解码/聚合与入口传输限制并验证真实 chunked 反例。RequestHeaderSize 对每个头名与其全部值累加字节,不是所有头的总和;超过 8KB 返回 431,默认字段为 16,000 B、错误头默认 errorMessage。很多小头仍可触发 Netty 总头解析限制,且解析器可能先于路由拒绝。阈值测试使用 N-1/N/N+1 字节、重复头与无长度 body,不把 Java 字符数当 UTF-8 字节数。
固定实现:RequestSize L73 起、RequestHeaderSize L80 起。它们的输入检查不能替代上游业务校验,也不保证响应 body 有界。
协议头与代理信任:三个处理层不能混用
第一层是 Boot/入站 HTTP server 是否解释 Forwarded 头并改写请求的 scheme/host/remote address;第二层是 Gateway 的 RemoteAddressResolver 为谓词取哪个地址;第三层是出站 HttpHeadersFilter 删改实际发给上游的头。出站删头无法撤销入站层已经信任伪造头而形成的 redirect URI。应把三层配置作为一张信任图评审,不能只打开 server.forward-headers-strategy 就认为来源已验证。
HttpHeadersFilter 与普通 GatewayFilter 不同,NettyRoutingFilter 在发送前按 REQUEST 处理,响应接收时按 RESPONSE 处理。RemoveHopByHopHeadersFilter 默认删除下列 9 项,还会读取 Connection 的 token,把它们点名的头一起删除;Connection: X-Debug 加 X-Debug: secret 应使后者消失。自定义 spring.cloud.gateway.server.webflux.filter.remove-hop-by-hop.headers 是替换列表,若只写一个新头会丢掉原有协议保护。
原码连续摘录:RemoveHopByHopHeadersFilter.java,5.0.4,L39–L48;整文件 SHA-256 f692351cdb7d44627e5ce506518ffdb96fbbcc1d7b9d453b8886468c5a380b57。
public static final Set<String> HEADERS_REMOVED_ON_REQUEST = new HashSet<>(
Arrays.asList("connection", "keep-alive", "transfer-encoding", "te", "trailer", "proxy-authorization",
"proxy-authenticate", "x-application-context", "upgrade"
// these two are not listed in
// https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3
// "proxy-connection",
// "content-length",
));
private int order = Ordered.LOWEST_PRECEDENCE - 1;默认还含 x-application-context,不能只照旧文档 8 项。TransferEncodingNormalizationHeadersFilter 对 chunked 与 Content-Length 冲突移除长度;GRPCRequestHeadersFilter 在匹配 application/grpc 的请求上恢复 te: trailers(受 HTTP/2 装配条件影响),TrailerHeadersFilter 在 body 写完后传递 trailers;te/trailer/upgrade 不应当成普通业务头盲目保留。WS Upgrade 由专用路由器处理,gRPC 状态可在 trailer 中,HTTP 200 不能替代 gRPC 成功证据。源码HTTP 头实现目录与下一页协议章节承接各分支。
5.0.4 的 trusted-proxies 是 Java regex 全匹配,不是 CIDR。下面只示意已由连接策略确认的两个内部代理;实际字符串是否含端口、IPv6 括号须用本版本解析后的值验证:
spring:
cloud:
gateway:
server:
webflux:
trusted-proxies: '10\.20\.0\.(10|11)'
forwarded:
enabled: true
by:
enabled: false
x-forwarded:
enabled: true
for-enabled: true
host-enabled: true
port-enabled: true
proto-enabled: true
prefix-enabled: true
for-append: trueForwarded 处理标准的 for/host/proto,可选 by;XForwarded 对 for/host/port/proto/prefix 有各自 enabled/append 开关,默认追加时要核对可信链而不是将整串当客户端身份。trusted-proxies: 10.20.0.0/24 是错语法,.* 又会信任所有输入。全匹配源码如下:
原码连续摘录:TrustedProxies.java,5.0.4,L55–L59;整文件 SHA-256 c14ae46051173ee95682f955ccf274f477526ca5aeea9f597f60c391a7cbdfb2。
static TrustedProxies from(String trustedProxies) {
Assert.hasText(trustedProxies, "trustedProxies must not be empty");
Pattern pattern = Pattern.compile(trustedProxies);
return value -> pattern.matcher(value).matches();
}未设、空 trusted-proxies,或关闭对应 forwarded/x-forwarded.enabled 时,不是“生成器关闭后原样透传”,而会装配删除 Forwarded / X-Forwarded 的 header filter:
原码连续摘录:GatewayAutoConfiguration.java,5.0.4,L336–L375;整文件 SHA-256 4258b696652d6b527a139aff14b503151821c3592fec897fd24c79d609af7fd9。
@Conditional(TrustedProxies.ForwardedTrustedProxiesCondition.class)
public ForwardedHeadersFilter forwardedHeadersFilter(Environment env, ServerProperties serverProperties,
GatewayProperties properties) {
boolean forwardedByEnabled = env.getProperty("spring.cloud.gateway.server.webflux.forwarded.by.enabled",
Boolean.class, false);
ForwardedHeadersFilter forwardedHeadersFilter = new ForwardedHeadersFilter(properties.getTrustedProxies());
forwardedHeadersFilter.setForwardedByEnabled(forwardedByEnabled);
forwardedHeadersFilter.setServerPort(serverProperties.getPort());
return forwardedHeadersFilter;
}
@Bean
@Conditional(TrustedProxies.NotForwardedTrustedProxiesCondition.class)
public RemoveForwardedHeadersFilter removeForwardedHeadersFilter() {
return new RemoveForwardedHeadersFilter();
}
// HttpHeaderFilter beans
@Bean
public RemoveHopByHopHeadersFilter removeHopByHopHeadersFilter() {
return new RemoveHopByHopHeadersFilter();
}
@Bean
@Conditional(TrustedProxies.XForwardedTrustedProxiesCondition.class)
public XForwardedHeadersFilter xForwardedHeadersFilter(GatewayProperties properties) {
return new XForwardedHeadersFilter(properties.getTrustedProxies());
}
@Bean
@Conditional(TrustedProxies.NotXForwardedTrustedProxiesCondition.class)
public RemoveXForwardedHeadersFilter removeXForwardedHeadersFilter() {
return new RemoveXForwardedHeadersFilter();
}
@Bean
@ConditionalOnProperty(name = "server.http2.enabled", matchIfMissing = true)
public GRPCRequestHeadersFilter gRPCRequestHeadersFilter() {
return new GRPCRequestHeadersFilter();因此在“无可信代理”的实验中提交伪造 Forwarded/XFF,上游预期看不到它们;这只证明出站清洗,不证明地址谓词和更外层 Security 没读过恶意输入。代理故障或拓扑从一跳变两跳时,maxTrustedIndex 不会自适应,必须同步连接允许名单、header 生成/清洗、resolver,并复验直连恶意 XFF、可信一跳、超跳、无信任删除与恢复。修复不可采用 trustAll 或 .*。
CORS:预检、未匹配路由和凭据
CORS 是浏览器跨源读取合同,不是认证、服务端访问控制或 CSRF 总开关。先选定全局或 route 级唯一策略,避免网关与上游分别追加冲突头。完整全局片段放在 spring.cloud.gateway.server.webflux 下:
globalcors:
add-to-simple-url-handler-mapping: true
cors-configurations:
'[/**]':
allowedOrigins: [https://study.example.test]
allowedMethods: [GET,POST,OPTIONS]
allowedHeaders: [Authorization,Content-Type,X-Lab-Version]
allowCredentials: true
maxAge: 300浏览器 Origin=study.example.test 的 OPTIONS 带 Access-Control-Request-Method=POST 时,应得到允许该源/方法的预检响应;基线路由仅 Method=GET,本来不匹配 OPTIONS,add-to-simple-url-handler-mapping=true 让未被 route 匹配的预检也能由简单映射处理。它不会给原本不存在的业务 route 创造 POST 转发能力。真实请求仍要认证,未允许 Origin 不应收到可供该源读取的授权响应。
route 级独立用法是 metadata.cors:
metadata:
cors:
allowedOrigins: [https://study.example.test]
allowedMethods: [GET]
allowedHeaders: ['*']
allowCredentials: false
maxAge: 60没有 Path 谓词时 route CORS 的路径适用范围需要额外核对,不能靠 Host 谓词推断路径隔离。带凭据时不能用 allowedOrigins='*';allowCredentials=true 不是匿名访问开关。Security 应处理 CORS 并允许必要预检通过认证门禁前的适当层,仍保护实际业务;浏览器防护和 cookie/CSRF 不变量回查 U31。固定全局及 route CORS是具体配置依据。
路由发布与边缘转换的容量选择
案例一:发布窗口与匹配成本。 假设 6 个网关副本,1,200 条路由,每条平均定义 2 KiB,10,000 请求/秒,平均检查 80 条才命中;一次完整定义约 2.34 MiB/副本、14.1 MiB/六副本,尚不含对象/regex 开销;每秒约 800,000 次 route 评估。时间/内容/Host/Path/Method/地址/版本/Weight/ReadBody 都影响检查次数或评估成本,不能把路由数当吞吐容量。发布不变量是任何节点实际执行的版本可追踪,坏定义不让必要入口静默消失,30 秒观察窗内完成六节点核对;在途请求允许按旧版结束。
合理方案 A 是不可变配置随构建发布,先单副本验证,再分批重启;成本是发布慢和同时保留两版资源,优点是版本/回滚清晰。方案 B 是共享 repository 加应用级版本发布协议:离线解析/校验完整集合、写版本标记、逐节点刷新与确认,保留上一版可重新发布;成本是发布控制器、权限、重复/乱序 refresh 与部分成功恢复责任。Redis 只共享定义,不能省略这些成本。平台负责人负责节点版本及回滚,业务负责人确认路径/版本与 schema 兼容。两者都要压测 p99 匹配耗时、评估次数、刷新期间错误与内存峰值;若 30 秒无法确认或 p99 匹配超过 5ms,先减少重叠路由、前置便宜谓词/稳定入口,再考虑拆分网关,不能仅开 filter 缓存。ReadBody 参与匹配将内存成本带到候选路由阶段,应移向专门路由或上游,详见下一页的 body 选择。
案例二:边缘转换与代理信任。 假设 20,000 请求/秒,平均请求头 2 KiB、响应头 1 KiB,持续 60 秒;仅头数据就约 58.6 MiB/s,不含复制、压缩和 TLS。若为每个请求记完整头,单分钟原始日志约 3.43 GiB,还会泄露 token。涉及路径/目标、Add/Set/Remove/Map、query、Location/Redirect/Status、大小上限、协议头与 CORS 的共同不变量是:上游拿到约定 canonical path/host,外来身份与动态目标头不能越过信任边界,重复值不改变授权,重定向不泄露内部地址。
合理方案 A 在网关集中做少量、确定的转换:固定目标映射、有限 regex、明确大小限制,唯一一层生成转发与 CORS 头;成本是网关成为合同中心,每次变更需逐服务回显回归。方案 B 让网关只选路由/清洗边界,把协议/字段适配交专用 BFF 或上游;成本是额外一跳、另一个部署和约定演进,但复杂 body/版本适配可独立扩容。每加 3ms BFF 等待,在 20,000/s 稳态下平均增加约 60 个在途;这不是免费解耦。网络负责人维护可信 peer/跳数,安全负责人定义允许传递头,API 负责人维护 path/query/Location 与兼容合同;恢复时回退一版转换规则,不能临时放宽 trusted-proxies。以真实重复头、编码路径、伪造 XFF、CORS 预检与超限输入验证后,再用有限样本脱敏日志观察 60 秒 p99、CPU、分配率、431/413 与上游接收值。头体积或 regex CPU 超预算时转向 BFF/减少转换,不能用无界 header 和全量 wiretap“先排查”。
验证入口、范围冻结与运行边界
冻结范围:本页主承接 G02–G07,并给 G01 四形态概览;G08–G14、G17 在下一页。G03 的每个官方谓词和 CloudFoundry 补项,G05 的每个路径/URI/Host/header/query/status/redirect/大小工厂均在上述流程给出用法与分支;body 工厂的唯一主讲解在下一页。G15/G16 由另作者承担。官方模块中的 dependencies、starter、sample、integration-tests、docs 是版本/示例/证据角色,不作为完成数量;配置参考、术语、shortcut/展开参数、Java DSL、metadata、共享路由、自定义三类扩展已在相应段落承接。
公开入口 python3 labs/cloud-gateway/run.py --redis-port=<本机Redis端口> 已真实运行 labs/cloud-gateway/webflux 的 12 个 HTTP 用例;命令、退出码、四模块计数与观察行见运行证据,入口与边界见实验说明。本页涉及的行为按以下边界分开:
- 路由输入(已运行):
/api/items/7?x=1命中 first 路由并带X-Lab-Label=first,上游 echopath=/items/7 query=x=1;/not-a-route404,随后合法请求恢复 200。 - 转换与顺序(已运行):
G0/R10/G20/R30正序执行、回程R30/G20/R10/G0;伪造X-Forwarded-For/Forwarded在真实上游被清除;超时路由 504 后恢复 200。 - 代理与身份(已运行):JWT 缺失/坏签名/错 iss/错 aud/过期 401、scope 不足 403、伪造身份头被删后由 Principal 重建、下游仍要求 Bearer。
- 定义发布、CORS 预检、短路与大小边界(未运行):动态路由推送、
RefreshRoutesEvent逐节点确认、OPTIONS 未匹配路由、RequestSize无 Content-Length 分支均按固定源码与配置推演,不冒充运行通过。 - 资源(未运行):大 body、多小头与单大头、body 缓存与
DataBuffer引用计数分支未运行,见下一页 body 章节。
闭卷验收
本页 18 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。