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:
+ *
+ * - a specific domain, e.g. {@code "https://domain1.com"}
+ *
- coma-delimited list of specific domains, 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.
+ *
- the CORS defined special value {@code "*"} for all origins
+ *
* 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");
}