diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 1eee79898c..561aa26b7e 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -176,7 +177,9 @@ public class CorsConfiguration { * it always sets the {@code Access-Control-Allow-Origin} response header to * the matched origin and never to {@code "*"}, nor to any other pattern, and * therefore can be used in combination with {@link #setAllowCredentials} - * set to {@code true}. + * set to {@code true}. Patterns also support list of allowed ports for origin, + * e.g. {@code "https://*.domain1.com:[8080,8081]"}. Additionally wildcard is supported + * in case any generic port should be allowed {@code "https://*.domain1.com:[*]"}. *
By default this is not set. * @since 5.3 */ @@ -647,6 +650,8 @@ public class CorsConfiguration { */ private static class OriginPattern { + private static final Pattern PORT_LIST_PATTERN = Pattern.compile("(.*):\\[(\\*|[\\d,]+)]"); + private final String declaredPattern; private final Pattern pattern; @@ -657,8 +662,25 @@ public class CorsConfiguration { } private static Pattern toPattern(String patternValue) { + //if pattern ends with allowed ports list + Matcher matcher = PORT_LIST_PATTERN.matcher(patternValue); + String portList = null; + if (matcher.matches()) { + patternValue = matcher.group(1); + portList = matcher.group(2); + } + patternValue = "\\Q" + patternValue + "\\E"; patternValue = patternValue.replace("*", "\\E.*\\Q"); + + if (ALL.equals(portList)) { + //there is a corner case. If '*' is specified, then origins with implicit default port (e.g. "https://test.com") should also match. + patternValue += "(:\\d+)?"; + } + else if (portList != null) { + patternValue += ":(" + portList.replace(',', '|') + ")"; + } + return Pattern.compile(patternValue); } diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index b920a9f167..256b9631df 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -335,6 +335,15 @@ public class CorsConfigurationTests { config.addAllowedOriginPattern("https://*.domain.com"); assertThat(config.checkOrigin("https://example.domain.com")).isEqualTo("https://example.domain.com"); + config.addAllowedOriginPattern("https://*.port.domain.com:[*]"); + assertThat(config.checkOrigin("https://example.port.domain.com")).isEqualTo("https://example.port.domain.com"); + assertThat(config.checkOrigin("https://example.port.domain.com:1234")).isEqualTo("https://example.port.domain.com:1234"); + + config.addAllowedOriginPattern("https://*.specific.port.com:[8080,8081]"); + assertThat(config.checkOrigin("https://example.specific.port.com:8080")).isEqualTo("https://example.specific.port.com:8080"); + assertThat(config.checkOrigin("https://example.specific.port.com:8081")).isEqualTo("https://example.specific.port.com:8081"); + assertThat(config.checkOrigin("https://example.specific.port.com:1234")).isNull(); + config.setAllowCredentials(false); assertThat(config.checkOrigin("https://example.domain.com")).isEqualTo("https://example.domain.com"); } @@ -352,6 +361,9 @@ public class CorsConfigurationTests { config.setAllowedOriginPatterns(new ArrayList<>()); assertThat(config.checkOrigin("https://domain.com")).isNull(); + + config.setAllowedOriginPatterns(Collections.singletonList("https://*.specific.port.com:[8080,8081]")); + assertThat(config.checkOrigin("https://example.specific.port.com:1234")).isNull(); } @Test