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 55828b4d503..e92ccd04e29 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 @@ -21,11 +21,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; -import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; @@ -120,9 +119,16 @@ public class CorsConfiguration { /** - * A list of origins for which cross-origin requests are allowed. Values may - * be a specific domain, e.g. {@code "https://domain1.com"}, or the CORS - * defined special value {@code "*"} for all origins. + * A list of origins for which cross-origin requests are allowed where each + * value may be one of the following: + * *

For matched pre-flight and actual requests the * {@code Access-Control-Allow-Origin} response header is set either to the * matched domain value or to {@code "*"}. Keep in mind however that the @@ -135,8 +141,15 @@ public class CorsConfiguration { * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. */ public void setAllowedOrigins(@Nullable List origins) { - this.allowedOrigins = (origins == null ? null : - origins.stream().filter(Objects::nonNull).map(this::trimTrailingSlash).collect(Collectors.toList())); + if (origins == null) { + this.allowedOrigins = null; + } + else { + this.allowedOrigins = new ArrayList<>(origins.size()); + for (String origin : origins) { + addAllowedOrigin(origin); + } + } } private String trimTrailingSlash(String origin) { @@ -164,8 +177,10 @@ public class CorsConfiguration { else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { setAllowedOrigins(DEFAULT_PERMIT_ALL); } - origin = trimTrailingSlash(origin); - this.allowedOrigins.add(origin); + parseComaDelimitedOrigin(origin, value -> { + value = trimTrailingSlash(value); + this.allowedOrigins.add(value); + }); } /** @@ -178,12 +193,16 @@ public class CorsConfiguration { * domain1.com on port 8080 or port 8081 *

  • {@literal https://*.domain1.com:[*]} -- domains ending with * domain1.com on any port, including the default port + *
  • coma-delimited list of patters, e.g. + * {@code "https://*.a1.com,https://*.a2.com"}; this is convenient when a + * value is resolved through a property placeholder, e.g. {@code "${origin}"}; + * note that such placeholders must be resolved externally. * *

    In contrast to {@link #setAllowedOrigins(List) allowedOrigins} which * only supports "*" and cannot be used with {@code allowCredentials}, when * an allowedOriginPattern is matched, the {@code Access-Control-Allow-Origin} * response header is set to the matched origin and not to {@code "*"} nor - * to the pattern. Therefore allowedOriginPatterns can be used in combination + * to the pattern. Therefore, allowedOriginPatterns can be used in combination * with {@link #setAllowCredentials} set to {@code true}. *

    By default this is not set. * @since 5.3 @@ -226,10 +245,40 @@ public class CorsConfiguration { if (this.allowedOriginPatterns == null) { this.allowedOriginPatterns = new ArrayList<>(4); } - originPattern = trimTrailingSlash(originPattern); - this.allowedOriginPatterns.add(new OriginPattern(originPattern)); - if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { - this.allowedOrigins = null; + parseComaDelimitedOrigin(originPattern, value -> { + value = trimTrailingSlash(value); + this.allowedOriginPatterns.add(new OriginPattern(value)); + if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { + this.allowedOrigins = null; + } + }); + } + + private static void parseComaDelimitedOrigin(String rawValue, Consumer valueConsumer) { + if (rawValue.indexOf(',') == -1) { + valueConsumer.accept(rawValue); + return; + } + int start = 0; + boolean withinPortRange = false; + for (int current = 0; current < rawValue.length(); current++) { + switch (rawValue.charAt(current)) { + case '[': + withinPortRange = true; + break; + case ']': + withinPortRange = false; + break; + case ',': + if (!withinPortRange) { + valueConsumer.accept(rawValue.substring(start, current).trim()); + start = current + 1; + } + break; + } + } + if (start < rawValue.length()) { + valueConsumer.accept(rawValue.substring(start)); } } 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 9549902149e..17945152289 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -291,6 +291,11 @@ class CorsConfigurationTests { config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // coma-delimited origins list + config.setAllowedOrigins(Collections.singletonList("https://a1.com,https://a2.com")); + assertThat(config.checkOrigin("https://a1.com")).isEqualTo("https://a1.com"); + assertThat(config.checkOrigin("https://a2.com/")).isEqualTo("https://a2.com/"); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); @@ -344,6 +349,12 @@ class CorsConfigurationTests { 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.addAllowedOriginPattern("https://*.a1.com:[8080,8081],https://*.a2.com"); + assertThat(config.checkOrigin("https://example.a1.com:8080")).isEqualTo("https://example.a1.com:8080"); + assertThat(config.checkOrigin("https://example.a1.com:8081")).isEqualTo("https://example.a1.com:8081"); + assertThat(config.checkOrigin("https://example.a1.com:8082")).isNull(); + assertThat(config.checkOrigin("https://example.a2.com")).isEqualTo("https://example.a2.com"); + config.setAllowCredentials(false); assertThat(config.checkOrigin("https://example.domain.com")).isEqualTo("https://example.domain.com"); }