From 8f81ca827f01517b2cf0c67a8b2b9a9a3d94d918 Mon Sep 17 00:00:00 2001
From: qnnn
Date: Thu, 2 Apr 2026 13:11:27 +0800
Subject: [PATCH 1/2] Refactor route-specific CORS to be bound to route instead
of path.
Move CORS configuration reading from CorsGatewayFilterApplicationListener (pre-populated at startup by path) to RoutePredicateHandlerMapping#getCorsConfiguration (read from route metadata during request). This allows independent CORS configurations for multiple routes sharing the same path predicate.
Signed-off-by: qnnn
---
.../config/GatewayAutoConfiguration.java | 11 +-
.../CorsGatewayFilterApplicationListener.java | 185 ----------------
.../handler/RoutePredicateHandlerMapping.java | 95 +++++++-
.../cloud/gateway/cors/CorsGlobalTests.java | 16 --
.../cloud/gateway/cors/CorsPerRouteTests.java | 79 ++++++-
...GatewayFilterApplicationListenerTests.java | 204 ------------------
.../application-cors-per-route-config.yml | 24 ++-
7 files changed, 187 insertions(+), 427 deletions(-)
delete mode 100644 spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListener.java
delete mode 100644 spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListenerTests.java
diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
index 934f190224..d72e1db916 100644
--- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
+++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
@@ -83,7 +83,6 @@
import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
import org.springframework.cloud.gateway.filter.WebsocketRoutingFilter;
import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter;
-import org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AddRequestHeadersIfNotPresentGatewayFilterFactory;
@@ -207,6 +206,7 @@
* @author Alberto C. Ríos
* @author Olga Maciaszek-Sharma
* @author FuYiNan Guo
+ * @author Nan Chiu
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.enabled", matchIfMissing = true)
@@ -292,15 +292,6 @@ public GlobalCorsProperties globalCorsProperties() {
return new GlobalCorsProperties();
}
- @Bean
- @ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.globalcors.enabled", matchIfMissing = true)
- public CorsGatewayFilterApplicationListener corsGatewayFilterApplicationListener(
- GlobalCorsProperties globalCorsProperties, RoutePredicateHandlerMapping routePredicateHandlerMapping,
- RouteLocator routeLocator) {
- return new CorsGatewayFilterApplicationListener(globalCorsProperties, routePredicateHandlerMapping,
- routeLocator);
- }
-
@Bean
@ConditionalOnMissingBean
public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHandler webHandler,
diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListener.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListener.java
deleted file mode 100644
index 97bad423f0..0000000000
--- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListener.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright 2013-present the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.cloud.gateway.filter.cors;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.springframework.cloud.gateway.config.GlobalCorsProperties;
-import org.springframework.cloud.gateway.event.RefreshRoutesResultEvent;
-import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
-import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
-import org.springframework.cloud.gateway.route.Route;
-import org.springframework.cloud.gateway.route.RouteLocator;
-import org.springframework.context.ApplicationListener;
-import org.springframework.web.cors.CorsConfiguration;
-
-/**
- *
- * For each {@link Route}, this listener inspects its predicates and looks for an instance
- * of {@link PathRoutePredicateFactory}. If a path predicate is found, the first defined
- * path pattern is extracted and used as the key for associating the route-specific
- * {@link CorsConfiguration}.
- *
- *
- *
- * After collecting all route-level CORS configurations, the listener merges them with
- * globally defined configurations from {@link GlobalCorsProperties}, ensuring that
- * route-specific configurations take precedence over global ones in case of conflicts
- * (e.g., both defining CORS rules for {@code /**}).
- *
- *
- *
- * The merged configuration map is then applied to the
- * {@link RoutePredicateHandlerMapping} via {@code setCorsConfigurations}.
- *
- *
- *
- * Note: A {@link LinkedHashMap} is used to store the merged configurations to preserve
- * insertion order, which ensures predictable CORS resolution when multiple path patterns
- * could match a request.
- *
- *
- * @author Fredrich Ombico
- * @author Abel Salgado Romero
- * @author Yavor Chamov
- */
-public class CorsGatewayFilterApplicationListener implements ApplicationListener {
-
- private final GlobalCorsProperties globalCorsProperties;
-
- private final RoutePredicateHandlerMapping routePredicateHandlerMapping;
-
- private final RouteLocator routeLocator;
-
- private static final String METADATA_KEY = "cors";
-
- private static final String ALL_PATHS = "/**";
-
- public CorsGatewayFilterApplicationListener(GlobalCorsProperties globalCorsProperties,
- RoutePredicateHandlerMapping routePredicateHandlerMapping, RouteLocator routeLocator) {
- this.globalCorsProperties = globalCorsProperties;
- this.routePredicateHandlerMapping = routePredicateHandlerMapping;
- this.routeLocator = routeLocator;
- }
-
- @Override
- public void onApplicationEvent(RefreshRoutesResultEvent event) {
- routeLocator.getRoutes().collectList().subscribe(routes -> {
- // pre-populate with pre-existing global cors configurations to combine with.
- Map corsConfigurations = new LinkedHashMap<>();
-
- routes.forEach(route -> {
- Optional corsConfiguration = getCorsConfiguration(route);
- corsConfiguration.ifPresent(configuration -> {
- String pathPredicate = getPathPredicate(route);
- corsConfigurations.put(pathPredicate, configuration);
- });
- });
-
- globalCorsProperties.getCorsConfigurations().forEach((path, config) -> {
- if (!corsConfigurations.containsKey(path)) {
- corsConfigurations.put(path, config);
- }
- });
- routePredicateHandlerMapping.setCorsConfigurations(corsConfigurations);
- });
- }
-
- /**
- * Finds the first path predicate and first pattern in the config.
- * @param route The Route to use.
- * @return the first path predicate pattern or /**.
- */
- private String getPathPredicate(Route route) {
- var predicate = route.getPredicate();
- var pathPatterns = new AtomicReference();
- predicate.accept(p -> {
- if (p.getConfig() instanceof PathRoutePredicateFactory.Config pathConfig) {
- if (!pathConfig.getPatterns().isEmpty()) {
- String firstPattern = pathConfig.getPatterns().get(0);
- if (pathPatterns.get() == null) {
- pathPatterns.set(firstPattern);
- }
- }
- }
- });
- String result = pathPatterns.get();
- if (result != null) {
- return result;
- }
- return ALL_PATHS;
- }
-
- @SuppressWarnings("unchecked")
- private Optional getCorsConfiguration(Route route) {
- Map corsMetadata = (Map) route.getMetadata().get(METADATA_KEY);
- if (corsMetadata != null) {
- final CorsConfiguration corsConfiguration = new CorsConfiguration();
-
- findValue(corsMetadata, "allowCredentials")
- .ifPresent(value -> corsConfiguration.setAllowCredentials((Boolean) value));
- findValue(corsMetadata, "allowedHeaders")
- .ifPresent(value -> corsConfiguration.setAllowedHeaders(asList(value)));
- findValue(corsMetadata, "allowedMethods")
- .ifPresent(value -> corsConfiguration.setAllowedMethods(asList(value)));
- findValue(corsMetadata, "allowedOriginPatterns")
- .ifPresent(value -> corsConfiguration.setAllowedOriginPatterns(asList(value)));
- findValue(corsMetadata, "allowedOrigins")
- .ifPresent(value -> corsConfiguration.setAllowedOrigins(asList(value)));
- findValue(corsMetadata, "exposedHeaders")
- .ifPresent(value -> corsConfiguration.setExposedHeaders(asList(value)));
- findValue(corsMetadata, "maxAge").ifPresent(value -> corsConfiguration.setMaxAge(asLong(value)));
-
- return Optional.of(corsConfiguration);
- }
-
- return Optional.empty();
- }
-
- private Optional