Updates to CORS patterns contribution

Closes gh-25016
This commit is contained in:
Rossen Stoyanchev
2020-07-08 13:18:11 +03:00
parent 1181bb1852
commit 0e4e25d227
24 changed files with 488 additions and 256 deletions

View File

@@ -21,6 +21,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.cors.CorsConfiguration;
@@ -77,37 +78,20 @@ public @interface CrossOrigin {
String[] value() default {};
/**
* The list of allowed origins that be specific origins, e.g.
* {@code "https://domain1.com"}, or {@code "*"} for all origins.
* <p>A matched origin is listed in the {@code Access-Control-Allow-Origin}
* response header of preflight actual CORS requests.
* <p>By default all origins are allowed.
* <p><strong>Note:</strong> CORS checks use values from "Forwarded"
* (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>),
* "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers,
* if present, in order to reflect the client-originated address.
* Consider using the {@code ForwardedHeaderFilter} in order to choose from a
* central place whether to extract and use, or to discard such headers.
* See the Spring Framework reference for more on this filter.
* @see #value
* A list of origins for which cross-origin requests are allowed. Please,
* see {@link CorsConfiguration#setAllowedOrigins(List)} for details.
* <p>By default all origins are allowed unless {@code originPatterns} is
* also set in which case {@code originPatterns} is used instead.
*/
@AliasFor("value")
String[] origins() default {};
/**
* The list of allowed origins patterns that be specific origins, e.g.
* {@code ".*\.domain1\.com"}, or {@code ".*"} for matching all origins.
* <p>A matched origin is listed in the {@code Access-Control-Allow-Origin}
* response header of preflight actual CORS requests.
* <p>By default all origins are allowed.
* <p><strong>Note:</strong> CORS checks use values from "Forwarded"
* (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>),
* "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers,
* if present, in order to reflect the client-originated address.
* Consider using the {@code ForwardedHeaderFilter} in order to choose from a
* central place whether to extract and use, or to discard such headers.
* See the Spring Framework reference for more on this filter.
* @see #value
* Alternative to {@link #origins()} that supports origins declared via
* wildcard patterns. Please, see
* @link CorsConfiguration#setAllowedOriginPatterns(List)} for details.
* <p>By default this is not set.
* @since 5.3
*/
String[] originPatterns() default {};

View File

@@ -54,8 +54,17 @@ public class CorsConfiguration {
/** Wildcard representing <em>all</em> origins, methods, or headers. */
public static final String ALL = "*";
/** Wildcard representing pattern that matches <em>all</em> origins. */
public static final String ALL_PATTERN = ".*";
private static final List<String> ALL_LIST = Collections.unmodifiableList(
Collections.singletonList(ALL));
private static final OriginPattern ALL_PATTERN = new OriginPattern("*");
private static final List<OriginPattern> ALL_PATTERN_LIST = Collections.unmodifiableList(
Collections.singletonList(ALL_PATTERN));
private static final List<String> DEFAULT_PERMIT_ALL = Collections.unmodifiableList(
Collections.singletonList(ALL));
private static final List<HttpMethod> DEFAULT_METHODS = Collections.unmodifiableList(
Arrays.asList(HttpMethod.GET, HttpMethod.HEAD));
@@ -63,21 +72,12 @@ public class CorsConfiguration {
private static final List<String> DEFAULT_PERMIT_METHODS = Collections.unmodifiableList(
Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()));
private static final List<String> DEFAULT_PERMIT_ALL = Collections.unmodifiableList(
Collections.singletonList(ALL));
private static final List<String> DEFAULT_PERMIT_ALL_PATTERN_STR = Collections.unmodifiableList(
Collections.singletonList(ALL_PATTERN));
private static final List<Pattern> DEFAULT_PERMIT_ALL_PATTERN = Collections.unmodifiableList(
Collections.singletonList(Pattern.compile(ALL_PATTERN)));
@Nullable
private List<String> allowedOrigins;
@Nullable
private List<Pattern> allowedOriginPatterns;
private List<OriginPattern> allowedOriginPatterns;
@Nullable
private List<String> allowedMethods;
@@ -123,9 +123,19 @@ public class CorsConfiguration {
/**
* Set the origins to allow, e.g. {@code "https://domain1.com"}.
* <p>The special value {@code "*"} allows all domains.
* <p>By default this is not set.
* 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.
* <p>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
* CORS spec does not allow {@code "*"} when {@link #setAllowCredentials
* allowCredentials} is set to {@code true} and as of 5.3 that combination
* is rejected in favor of using {@link #setAllowedOriginPatterns
* allowedOriginPatterns} instead.
* <p>By default this is not set which means that no origins are allowed.
* However an instance of this class is often initialized further, e.g. for
* {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}.
*/
public void setAllowedOrigins(@Nullable List<String> allowedOrigins) {
this.allowedOrigins = (allowedOrigins != null ? new ArrayList<>(allowedOrigins) : null);
@@ -133,8 +143,6 @@ public class CorsConfiguration {
/**
* Return the configured origins to allow, or {@code null} if none.
* @see #addAllowedOrigin(String)
* @see #setAllowedOrigins(List)
*/
@Nullable
public List<String> getAllowedOrigins() {
@@ -142,21 +150,29 @@ public class CorsConfiguration {
}
/**
* Add an origin to allow.
* Variant of {@link #setAllowedOrigins} for adding one origin at a time.
*/
public void addAllowedOrigin(String origin) {
if (this.allowedOrigins == null) {
this.allowedOrigins = new ArrayList<>(4);
}
else if (this.allowedOrigins == DEFAULT_PERMIT_ALL) {
else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) {
setAllowedOrigins(DEFAULT_PERMIT_ALL);
}
this.allowedOrigins.add(origin);
}
/**
* Set the origins patterns to allow, e.g. {@code "*.com"}.
* Alternative to {@link #setAllowedOrigins} that supports origins declared
* via wildcard patterns. In contrast to {@link #setAllowedOrigins allowedOrigins}
* which does support the special value {@code "*"}, this property allows
* more flexible patterns, e.g. {@code "https://*.domain1.com"}. Furthermore
* 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}.
* <p>By default this is not set.
* @since 5.3
*/
public CorsConfiguration setAllowedOriginPatterns(@Nullable List<String> allowedOriginPatterns) {
if (allowedOriginPatterns == null) {
@@ -164,42 +180,39 @@ public class CorsConfiguration {
}
else {
this.allowedOriginPatterns = new ArrayList<>(allowedOriginPatterns.size());
for (String pattern : allowedOriginPatterns) {
this.allowedOriginPatterns.add(Pattern.compile(pattern));
for (String patternValue : allowedOriginPatterns) {
addAllowedOriginPattern(patternValue);
}
}
return this;
}
/**
* Return the configured origins patterns to allow, or {@code null} if none.
*
* @see #addAllowedOriginPattern(String)
* @see #setAllowedOriginPatterns(List)
* @since 5.3
*/
@Nullable
public List<String> getAllowedOriginPatterns() {
if (this.allowedOriginPatterns == null) {
return null;
}
if (this.allowedOriginPatterns == DEFAULT_PERMIT_ALL_PATTERN) {
return DEFAULT_PERMIT_ALL_PATTERN_STR;
}
return this.allowedOriginPatterns.stream().map(Pattern::toString).collect(Collectors.toList());
return this.allowedOriginPatterns.stream()
.map(OriginPattern::getDeclaredPattern)
.collect(Collectors.toList());
}
/**
* Add an origin pattern to allow.
* Variant of {@link #setAllowedOriginPatterns} for adding one origin at a time.
* @since 5.3
*/
public void addAllowedOriginPattern(String originPattern) {
if (this.allowedOriginPatterns == null) {
this.allowedOriginPatterns = new ArrayList<>(4);
}
else if (this.allowedOriginPatterns == DEFAULT_PERMIT_ALL_PATTERN) {
setAllowedOriginPatterns(DEFAULT_PERMIT_ALL_PATTERN_STR);
this.allowedOriginPatterns.add(new OriginPattern(originPattern));
if (this.allowedOrigins == DEFAULT_PERMIT_ALL) {
this.allowedOrigins = null;
}
this.allowedOriginPatterns.add(Pattern.compile(originPattern));
}
/**
@@ -397,16 +410,15 @@ public class CorsConfiguration {
/**
* By default a newly created {@code CorsConfiguration} does not permit any
* cross-origin requests and must be configured explicitly to indicate what
* should be allowed.
* <p>Use this method to flip the initialization model to start with open
* defaults that permit all cross-origin requests for GET, HEAD, and POST
* requests. Note however that this method will not override any existing
* values already set.
* <p>The following defaults are applied if not already set:
* By default {@code CorsConfiguration} does not permit any cross-origin
* requests and must be configured explicitly. Use this method to switch to
* defaults that permit all cross-origin requests for GET, HEAD, and POST,
* but not overriding any values that have already been set.
* <p>The following defaults are applied for values that are not set:
* <ul>
* <li>Allow all origins.</li>
* <li>Allow all origins with the special value {@code "*"} defined in the
* CORS spec. This is set only if neither {@link #setAllowedOrigins origins}
* nor {@link #setAllowedOriginPatterns originPatterns} are already set.</li>
* <li>Allow "simple" methods {@code GET}, {@code HEAD} and {@code POST}.</li>
* <li>Allow all headers.</li>
* <li>Set max age to 1800 seconds (30 minutes).</li>
@@ -430,6 +442,26 @@ public class CorsConfiguration {
return this;
}
/**
* Validate that when {@link #setAllowCredentials allowCredentials} is true,
* {@link #setAllowedOrigins allowedOrigins} does not contain the special
* value {@code "*"} since in that case the "Access-Control-Allow-Origin"
* cannot be set to {@code "*"}.
* @throws IllegalArgumentException if the validation fails
* @since 5.3
*/
public void validateAllowCredentials() {
if (this.allowCredentials == Boolean.TRUE &&
this.allowedOrigins != null && this.allowedOrigins.contains(ALL)) {
throw new IllegalArgumentException(
"When allowCredentials is true, allowedOrigins cannot contain the special value \"*\"" +
"since that cannot be set on the \"Access-Control-Allow-Origin\" response header. " +
"To allow credentials to a set of origins, list them explicitly " +
"or consider using \"allowedOriginPatterns\" instead.");
}
}
/**
* Combine the non-null properties of the supplied
* {@code CorsConfiguration} with this one.
@@ -439,12 +471,11 @@ public class CorsConfiguration {
* <p>Combining lists like {@code allowedOrigins}, {@code allowedMethods},
* {@code allowedHeaders} or {@code exposedHeaders} is done in an additive
* way. For example, combining {@code ["GET", "POST"]} with
* {@code ["PATCH"]} results in {@code ["GET", "POST", "PATCH"]}, but keep
* in mind that combining {@code ["GET", "POST"]} with {@code ["*"]}
* results in {@code ["*"]}.
* <p>Notice that default permit values set by
* {@code ["PATCH"]} results in {@code ["GET", "POST", "PATCH"]}. However,
* combining {@code ["GET", "POST"]} with {@code ["*"]} results in
* {@code ["*"]}. Note also that default permit values set by
* {@link CorsConfiguration#applyPermitDefaultValues()} are overridden by
* any value explicitly defined.
* any explicitly defined values.
* @return the combined {@code CorsConfiguration}, or {@code this}
* configuration if the supplied configuration is {@code null}
*/
@@ -453,15 +484,12 @@ public class CorsConfiguration {
if (other == null) {
return this;
}
// Bypass setAllowedOrigins to avoid re-compiling patterns
CorsConfiguration config = new CorsConfiguration(this);
List<String> combinedOrigins = combine(getAllowedOrigins(), other.getAllowedOrigins());
List<String> combinedOriginPatterns = combine(getAllowedOriginPatterns(), other.getAllowedOriginPatterns());
if (combinedOrigins == DEFAULT_PERMIT_ALL && combinedOriginPatterns != DEFAULT_PERMIT_ALL_PATTERN_STR
&& !CollectionUtils.isEmpty(combinedOriginPatterns)) {
combinedOrigins = null;
}
config.setAllowedOrigins(combinedOrigins);
config.setAllowedOriginPatterns(combinedOriginPatterns);
List<String> origins = combine(getAllowedOrigins(), other.getAllowedOrigins());
List<OriginPattern> patterns = combinePatterns(this.allowedOriginPatterns, other.allowedOriginPatterns);
config.allowedOrigins = (origins == DEFAULT_PERMIT_ALL && !CollectionUtils.isEmpty(patterns) ? null : origins);
config.allowedOriginPatterns = patterns;
config.setAllowedMethods(combine(getAllowedMethods(), other.getAllowedMethods()));
config.setAllowedHeaders(combine(getAllowedHeaders(), other.getAllowedHeaders()));
config.setExposedHeaders(combine(getExposedHeaders(), other.getExposedHeaders()));
@@ -483,25 +511,40 @@ public class CorsConfiguration {
if (source == null) {
return other;
}
if (source == DEFAULT_PERMIT_ALL || source == DEFAULT_PERMIT_METHODS
|| source == DEFAULT_PERMIT_ALL_PATTERN_STR) {
if (source == DEFAULT_PERMIT_ALL || source == DEFAULT_PERMIT_METHODS) {
return other;
}
if (other == DEFAULT_PERMIT_ALL || other == DEFAULT_PERMIT_METHODS
|| other == DEFAULT_PERMIT_ALL_PATTERN_STR) {
if (other == DEFAULT_PERMIT_ALL || other == DEFAULT_PERMIT_METHODS) {
return source;
}
if (source.contains(ALL) || other.contains(ALL)) {
return new ArrayList<>(Collections.singletonList(ALL));
return ALL_LIST;
}
if ( source.contains(ALL_PATTERN) || other.contains(ALL_PATTERN)) {
return new ArrayList<>(Collections.singletonList(ALL_PATTERN));
}
Set<String> combined = new LinkedHashSet<>(source);
Set<String> combined = new LinkedHashSet<>(source.size() + other.size());
combined.addAll(source);
combined.addAll(other);
return new ArrayList<>(combined);
}
private List<OriginPattern> combinePatterns(
@Nullable List<OriginPattern> source, @Nullable List<OriginPattern> other) {
if (other == null) {
return (source != null ? source : Collections.emptyList());
}
if (source == null) {
return other;
}
if (source.contains(ALL_PATTERN) || other.contains(ALL_PATTERN)) {
return ALL_PATTERN_LIST;
}
Set<OriginPattern> combined = new LinkedHashSet<>(source.size() + other.size());
combined.addAll(source);
combined.addAll(other);
return new ArrayList<>(combined);
}
/**
* Check the origin of the request against the configured allowed origins.
* @param requestOrigin the origin to check
@@ -513,15 +556,10 @@ public class CorsConfiguration {
if (!StringUtils.hasText(requestOrigin)) {
return null;
}
if (!ObjectUtils.isEmpty(this.allowedOrigins)) {
if (this.allowedOrigins.contains(ALL)) {
if (this.allowCredentials != Boolean.TRUE) {
return ALL;
}
else {
return requestOrigin;
}
validateAllowCredentials();
return ALL;
}
for (String allowedOrigin : this.allowedOrigins) {
if (requestOrigin.equalsIgnoreCase(allowedOrigin)) {
@@ -530,21 +568,12 @@ public class CorsConfiguration {
}
}
if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) {
for (Pattern allowedOriginsPattern : this.allowedOriginPatterns) {
if (allowedOriginsPattern.pattern().equals(ALL_PATTERN)) {
if (this.allowCredentials != Boolean.TRUE) {
return ALL;
}
else {
return requestOrigin;
}
}
else if (allowedOriginsPattern.matcher(requestOrigin).matches()) {
for (OriginPattern p : this.allowedOriginPatterns) {
if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(requestOrigin).matches()) {
return requestOrigin;
}
}
}
return null;
}
@@ -608,4 +637,57 @@ public class CorsConfiguration {
return (result.isEmpty() ? null : result);
}
/**
* Contains both the user-declared pattern (e.g. "https://*.domain.com") and
* the regex {@link Pattern} derived from it.
*/
private static class OriginPattern {
private final String declaredPattern;
private final Pattern pattern;
OriginPattern(String declaredPattern) {
this.declaredPattern = declaredPattern;
this.pattern = toPattern(declaredPattern);
}
private static Pattern toPattern(String patternValue) {
patternValue = "\\Q" + patternValue + "\\E";
patternValue = patternValue.replace("*", "\\E.*\\Q");
return Pattern.compile(patternValue);
}
public String getDeclaredPattern() {
return this.declaredPattern;
}
public Pattern getPattern() {
return this.pattern;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || !getClass().equals(other.getClass())) {
return false;
}
return ObjectUtils.nullSafeEquals(
this.declaredPattern, ((OriginPattern) other).declaredPattern);
}
@Override
public int hashCode() {
return this.declaredPattern.hashCode();
}
@Override
public String toString() {
return this.declaredPattern;
}
}
}

View File

@@ -40,6 +40,8 @@ public class CorsConfigurationTests {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(null);
assertThat(config.getAllowedOrigins()).isNull();
config.setAllowedOriginPatterns(null);
assertThat(config.getAllowedOriginPatterns()).isNull();
config.setAllowedHeaders(null);
assertThat(config.getAllowedHeaders()).isNull();
config.setAllowedMethods(null);
@@ -50,42 +52,39 @@ public class CorsConfigurationTests {
assertThat(config.getAllowCredentials()).isNull();
config.setMaxAge((Long) null);
assertThat(config.getMaxAge()).isNull();
config.setAllowedOriginPatterns(null);
assertThat(config.getAllowedOriginPatterns()).isNull();
}
@Test
public void setValues() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
assertThat(config.getAllowedOrigins()).containsExactly("*");
config.addAllowedOriginPattern("http://*.example.com");
config.addAllowedHeader("*");
assertThat(config.getAllowedHeaders()).containsExactly("*");
config.addAllowedMethod("*");
assertThat(config.getAllowedMethods()).containsExactly("*");
config.addExposedHeader("header1");
config.addExposedHeader("header2");
assertThat(config.getExposedHeaders()).containsExactly("header1", "header2");
config.setAllowCredentials(true);
assertThat(config.getAllowCredentials()).isTrue();
config.setMaxAge(123L);
assertThat(config.getAllowedOrigins()).containsExactly("*");
assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.example.com");
assertThat(config.getAllowedHeaders()).containsExactly("*");
assertThat(config.getAllowedMethods()).containsExactly("*");
assertThat(config.getExposedHeaders()).containsExactly("header1", "header2");
assertThat(config.getAllowCredentials()).isTrue();
assertThat(config.getMaxAge()).isEqualTo(new Long(123));
config.addAllowedOriginPattern(".*\\.example\\.com");
assertThat(config.getAllowedOriginPatterns()).containsExactly(".*\\.example\\.com");
}
@Test
public void asteriskWildCardOnAddExposedHeader() {
CorsConfiguration config = new CorsConfiguration();
assertThatIllegalArgumentException().isThrownBy(() ->
config.addExposedHeader("*"));
assertThatIllegalArgumentException()
.isThrownBy(() -> new CorsConfiguration().addExposedHeader("*"));
}
@Test
public void asteriskWildCardOnSetExposedHeaders() {
CorsConfiguration config = new CorsConfiguration();
assertThatIllegalArgumentException()
.isThrownBy(() -> config.setExposedHeaders(Collections.singletonList("*")));
.isThrownBy(() -> new CorsConfiguration().setExposedHeaders(Collections.singletonList("*")));
}
@Test
@@ -94,28 +93,31 @@ public class CorsConfigurationTests {
config.setAllowedOrigins(Collections.singletonList("*"));
config.combine(null);
assertThat(config.getAllowedOrigins()).containsExactly("*");
assertThat(config.getAllowedOriginPatterns()).isNull();
}
@Test
public void combineWithNullProperties() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.setAllowedOriginPatterns(Collections.singletonList("http://*.example.com"));
config.addAllowedHeader("header1");
config.addExposedHeader("header3");
config.addAllowedMethod(HttpMethod.GET.name());
config.setMaxAge(123L);
config.setAllowCredentials(true);
config.setAllowedOriginPatterns(Collections.singletonList(".*\\.example\\.com"));
CorsConfiguration other = new CorsConfiguration();
config = config.combine(other);
assertThat(config).isNotNull();
assertThat(config.getAllowedOrigins()).containsExactly("*");
assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.example.com");
assertThat(config.getAllowedHeaders()).containsExactly("header1");
assertThat(config.getExposedHeaders()).containsExactly("header3");
assertThat(config.getAllowedMethods()).containsExactly(HttpMethod.GET.name());
assertThat(config.getMaxAge()).isEqualTo(new Long(123));
assertThat(config.getAllowCredentials()).isTrue();
assertThat(config.getAllowedOriginPatterns()).containsExactly(".*\\.example\\.com");
}
@Test // SPR-15772
@@ -157,35 +159,36 @@ public class CorsConfigurationTests {
public void combinePatternWithDefaultPermitValues() {
CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
CorsConfiguration other = new CorsConfiguration();
other.addAllowedOriginPattern(".*\\.com");
other.addAllowedOriginPattern("http://*.com");
CorsConfiguration combinedConfig = other.combine(config);
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).isNull();
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly(".*\\.com");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("http://*.com");
combinedConfig = config.combine(other);
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).isNull();
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly(".*\\.com");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("http://*.com");
}
@Test
public void combinePatternWithDefaultPermitValuesAndCustomOrigin() {
CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
config.setAllowedOrigins(Collections.singletonList("https://domain.com"));
CorsConfiguration other = new CorsConfiguration();
other.addAllowedOriginPattern(".*\\.com");
other.addAllowedOriginPattern("http://*.com");
CorsConfiguration combinedConfig = other.combine(config);
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly(".*\\.com");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("http://*.com");
combinedConfig = config.combine(other);
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly(".*\\.com");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("http://*.com");
}
@Test
@@ -194,25 +197,28 @@ public class CorsConfigurationTests {
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOriginPattern(".*");
config.addAllowedOriginPattern("*");
CorsConfiguration other = new CorsConfiguration();
other.addAllowedOrigin("https://domain.com");
other.addAllowedOriginPattern("http://*.company.com");
other.addAllowedHeader("header1");
other.addExposedHeader("header2");
other.addAllowedOriginPattern(".*\\.company\\.com");
other.addAllowedMethod(HttpMethod.PUT.name());
CorsConfiguration combinedConfig = config.combine(other);
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).containsExactly("*");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("*");
assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*");
assertThat(combinedConfig.getAllowedMethods()).containsExactly("*");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly(".*");
combinedConfig = other.combine(config);
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).containsExactly("*");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("*");
assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*");
assertThat(combinedConfig.getAllowedMethods()).containsExactly("*");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly(".*");
}
@Test // SPR-14792
@@ -226,41 +232,45 @@ public class CorsConfigurationTests {
config.addExposedHeader("header4");
config.addAllowedMethod(HttpMethod.GET.name());
config.addAllowedMethod(HttpMethod.PUT.name());
config.addAllowedOriginPattern(".*\\.domain1\\.com");
config.addAllowedOriginPattern(".*\\.domain2\\.com");
config.addAllowedOriginPattern("http://*.domain1.com");
config.addAllowedOriginPattern("http://*.domain2.com");
CorsConfiguration other = new CorsConfiguration();
other.addAllowedOrigin("https://domain1.com");
other.addAllowedOriginPattern("http://*.domain1.com");
other.addAllowedHeader("header1");
other.addExposedHeader("header3");
other.addAllowedMethod(HttpMethod.GET.name());
other.addAllowedOriginPattern(".*\\.domain1\\.com");
CorsConfiguration combinedConfig = config.combine(other);
assertThat(combinedConfig).isNotNull();
assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain1.com", "https://domain2.com");
assertThat(combinedConfig.getAllowedHeaders()).containsExactly("header1", "header2");
assertThat(combinedConfig.getExposedHeaders()).containsExactly("header3", "header4");
assertThat(combinedConfig.getAllowedMethods()).containsExactly(HttpMethod.GET.name(), HttpMethod.PUT.name());
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly(".*\\.domain1\\.com", ".*\\.domain2\\.com");
assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("http://*.domain1.com", "http://*.domain2.com");
}
@Test
public void combine() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("https://domain1.com");
config.addAllowedOriginPattern("http://*.domain1.com");
config.addAllowedHeader("header1");
config.addExposedHeader("header3");
config.addAllowedMethod(HttpMethod.GET.name());
config.setMaxAge(123L);
config.setAllowCredentials(true);
config.addAllowedOriginPattern(".*\\.domain1\\.com");
CorsConfiguration other = new CorsConfiguration();
other.addAllowedOrigin("https://domain2.com");
other.addAllowedOriginPattern("http://*.domain2.com");
other.addAllowedHeader("header2");
other.addExposedHeader("header4");
other.addAllowedMethod(HttpMethod.PUT.name());
other.setMaxAge(456L);
other.setAllowCredentials(false);
other.addAllowedOriginPattern(".*\\.domain2\\.com");
config = config.combine(other);
assertThat(config).isNotNull();
assertThat(config.getAllowedOrigins()).containsExactly("https://domain1.com", "https://domain2.com");
@@ -270,18 +280,21 @@ public class CorsConfigurationTests {
assertThat(config.getMaxAge()).isEqualTo(new Long(456));
assertThat(config).isNotNull();
assertThat(config.getAllowCredentials()).isFalse();
assertThat(config.getAllowedOriginPatterns()).containsExactly(".*\\.domain1\\.com", ".*\\.domain2\\.com");
assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.domain1.com", "http://*.domain2.com");
}
@Test
public void checkOriginAllowed() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*"));
config.addAllowedOrigin("*");
assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*");
config.setAllowCredentials(true);
assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com");
assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com"));
config.setAllowedOrigins(Collections.singletonList("https://domain.com"));
assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com");
config.setAllowCredentials(false);
assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com");
}
@@ -291,10 +304,13 @@ public class CorsConfigurationTests {
CorsConfiguration config = new CorsConfiguration();
assertThat(config.checkOrigin(null)).isNull();
assertThat(config.checkOrigin("https://domain.com")).isNull();
config.addAllowedOrigin("*");
assertThat(config.checkOrigin(null)).isNull();
config.setAllowedOrigins(Collections.singletonList("https://domain1.com"));
assertThat(config.checkOrigin("https://domain2.com")).isNull();
config.setAllowedOrigins(new ArrayList<>());
assertThat(config.checkOrigin("https://domain.com")).isNull();
}
@@ -302,12 +318,17 @@ public class CorsConfigurationTests {
@Test
public void checkOriginPatternAllowed() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(Collections.singletonList(".*"));
assertThat(config.checkOrigin("https://domain.com")).isNull();
config.applyPermitDefaultValues();
assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*");
config.setAllowCredentials(true);
assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com");
config.setAllowedOriginPatterns(Collections.singletonList(".*\\.domain\\.com"));
assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com"));
config.addAllowedOriginPattern("https://*.domain.com");
assertThat(config.checkOrigin("https://example.domain.com")).isEqualTo("https://example.domain.com");
config.setAllowCredentials(false);
assertThat(config.checkOrigin("https://example.domain.com")).isEqualTo("https://example.domain.com");
}
@@ -317,10 +338,12 @@ public class CorsConfigurationTests {
CorsConfiguration config = new CorsConfiguration();
assertThat(config.checkOrigin(null)).isNull();
assertThat(config.checkOrigin("https://domain.com")).isNull();
config.addAllowedOriginPattern(".*");
config.addAllowedOriginPattern("*");
assertThat(config.checkOrigin(null)).isNull();
config.setAllowedOriginPatterns(Collections.singletonList(".*\\.domain1\\.com"));
config.setAllowedOriginPatterns(Collections.singletonList("http://*.domain1.com"));
assertThat(config.checkOrigin("https://domain2.com")).isNull();
config.setAllowedOriginPatterns(new ArrayList<>());
assertThat(config.checkOrigin("https://domain.com")).isNull();
}
@@ -329,8 +352,10 @@ public class CorsConfigurationTests {
public void checkMethodAllowed() {
CorsConfiguration config = new CorsConfiguration();
assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET, HttpMethod.HEAD);
config.addAllowedMethod("GET");
assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET);
config.addAllowedMethod("POST");
assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET, HttpMethod.POST);
assertThat(config.checkHttpMethod(HttpMethod.POST)).containsExactly(HttpMethod.GET, HttpMethod.POST);
@@ -341,6 +366,7 @@ public class CorsConfigurationTests {
CorsConfiguration config = new CorsConfiguration();
assertThat(config.checkHttpMethod(null)).isNull();
assertThat(config.checkHttpMethod(HttpMethod.DELETE)).isNull();
config.setAllowedMethods(new ArrayList<>());
assertThat(config.checkHttpMethod(HttpMethod.POST)).isNull();
}
@@ -349,8 +375,10 @@ public class CorsConfigurationTests {
public void checkHeadersAllowed() {
CorsConfiguration config = new CorsConfiguration();
assertThat(config.checkHeaders(Collections.emptyList())).isEqualTo(Collections.emptyList());
config.addAllowedHeader("header1");
config.addAllowedHeader("header2");
assertThat(config.checkHeaders(Collections.singletonList("header1"))).containsExactly("header1");
assertThat(config.checkHeaders(Arrays.asList("header1", "header2"))).containsExactly("header1", "header2");
assertThat(config.checkHeaders(Arrays.asList("header1", "header2", "header3"))).containsExactly("header1", "header2");
@@ -361,8 +389,10 @@ public class CorsConfigurationTests {
CorsConfiguration config = new CorsConfiguration();
assertThat(config.checkHeaders(null)).isNull();
assertThat(config.checkHeaders(Collections.singletonList("header1"))).isNull();
config.setAllowedHeaders(Collections.emptyList());
assertThat(config.checkHeaders(Collections.singletonList("header1"))).isNull();
config.addAllowedHeader("header2");
config.addAllowedHeader("header3");
assertThat(config.checkHeaders(Collections.singletonList("header1"))).isNull();
@@ -374,6 +404,7 @@ public class CorsConfigurationTests {
config.addAllowedOrigin("https://domain.com");
config.addAllowedHeader("header1");
config.addAllowedMethod("PATCH");
assertThat(config.getAllowedOrigins()).containsExactly("*", "https://domain.com");
assertThat(config.getAllowedHeaders()).containsExactly("*", "header1");
assertThat(config.getAllowedMethods()).containsExactly("GET", "HEAD", "POST", "PATCH");
@@ -382,9 +413,10 @@ public class CorsConfigurationTests {
@Test
public void permitDefaultDoesntSetOriginWhenPatternPresent() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern(".*\\.com");
config.addAllowedOriginPattern("http://*.com");
config = config.applyPermitDefaultValues();
assertThat(config.getAllowedOrigins()).isNull();
assertThat(config.getAllowedOriginPatterns()).containsExactly(".*\\.com");
assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.com");
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.web.cors;
import java.util.Arrays;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
@@ -27,6 +29,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Test {@link DefaultCorsProcessor} with simple or preflight CORS request.
@@ -138,11 +141,17 @@ public class DefaultCorsProcessorTests {
}
@Test
public void actualRequestCredentialsWithOriginWildcard() throws Exception {
public void actualRequestCredentialsWithWildcardOrigin() throws Exception {
this.request.setMethod(HttpMethod.GET.name());
this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
this.conf.addAllowedOrigin("*");
this.conf.setAllowCredentials(true);
assertThatIllegalArgumentException()
.isThrownBy(() -> this.processor.processRequest(this.conf, this.request, this.response));
this.conf.setAllowedOrigins(null);
this.conf.addAllowedOriginPattern("*");
this.processor.processRequest(this.conf, this.request, this.response);
assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
@@ -311,17 +320,21 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestCredentialsWithOriginWildcard() throws Exception {
public void preflightRequestCredentialsWithWildcardOrigin() throws Exception {
this.request.setMethod(HttpMethod.OPTIONS.name());
this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Header1");
this.conf.addAllowedOrigin("https://domain1.com");
this.conf.addAllowedOrigin("*");
this.conf.addAllowedOrigin("http://domain3.example");
this.conf.setAllowedOrigins(Arrays.asList("https://domain1.com", "*", "http://domain3.example"));
this.conf.addAllowedHeader("Header1");
this.conf.setAllowCredentials(true);
assertThatIllegalArgumentException().isThrownBy(() ->
this.processor.processRequest(this.conf, this.request, this.response));
this.conf.setAllowedOrigins(null);
this.conf.addAllowedOriginPattern("*");
this.processor.processRequest(this.conf, this.request, this.response);
assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(this.response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com");

View File

@@ -29,6 +29,7 @@ import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRe
import org.springframework.web.testfixture.server.MockServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS;
@@ -56,7 +57,7 @@ public class DefaultCorsProcessorTests {
@Test
public void requestWithoutOriginHeader() throws Exception {
public void requestWithoutOriginHeader() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "http://domain1.example/test.html")
.build();
@@ -71,7 +72,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void sameOriginRequest() throws Exception {
public void sameOriginRequest() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "http://domain1.example/test.html")
.header(HttpHeaders.ORIGIN, "http://domain1.example")
@@ -87,7 +88,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void actualRequestWithOriginHeader() throws Exception {
public void actualRequestWithOriginHeader() {
ServerWebExchange exchange = actualRequest();
this.processor.process(this.conf, exchange);
@@ -99,7 +100,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void actualRequestWithOriginHeaderAndNullConfig() throws Exception {
public void actualRequestWithOriginHeaderAndNullConfig() {
ServerWebExchange exchange = actualRequest();
this.processor.process(null, exchange);
@@ -109,7 +110,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void actualRequestWithOriginHeaderAndAllowedOrigin() throws Exception {
public void actualRequestWithOriginHeaderAndAllowedOrigin() {
ServerWebExchange exchange = actualRequest();
this.conf.addAllowedOrigin("*");
this.processor.process(this.conf, exchange);
@@ -125,7 +126,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void actualRequestCredentials() throws Exception {
public void actualRequestCredentials() {
ServerWebExchange exchange = actualRequest();
this.conf.addAllowedOrigin("https://domain1.com");
this.conf.addAllowedOrigin("https://domain2.com");
@@ -144,10 +145,14 @@ public class DefaultCorsProcessorTests {
}
@Test
public void actualRequestCredentialsWithOriginWildcard() throws Exception {
public void actualRequestCredentialsWithWildcardOrigin() {
ServerWebExchange exchange = actualRequest();
this.conf.addAllowedOrigin("*");
this.conf.setAllowCredentials(true);
assertThatIllegalArgumentException().isThrownBy(() -> this.processor.process(this.conf, exchange));
this.conf.setAllowedOrigins(null);
this.conf.addAllowedOriginPattern("*");
this.processor.process(this.conf, exchange);
ServerHttpResponse response = exchange.getResponse();
@@ -161,7 +166,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void actualRequestCaseInsensitiveOriginMatch() throws Exception {
public void actualRequestCaseInsensitiveOriginMatch() {
ServerWebExchange exchange = actualRequest();
this.conf.addAllowedOrigin("https://DOMAIN2.com");
this.processor.process(this.conf, exchange);
@@ -174,7 +179,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void actualRequestExposedHeaders() throws Exception {
public void actualRequestExposedHeaders() {
ServerWebExchange exchange = actualRequest();
this.conf.addExposedHeader("header1");
this.conf.addExposedHeader("header2");
@@ -193,7 +198,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestAllOriginsAllowed() throws Exception {
public void preflightRequestAllOriginsAllowed() {
ServerWebExchange exchange = MockServerWebExchange.from(
preFlightRequest().header(ACCESS_CONTROL_REQUEST_METHOD, "GET"));
this.conf.addAllowedOrigin("*");
@@ -207,7 +212,7 @@ public class DefaultCorsProcessorTests {
@Test
public void preflightRequestWrongAllowedMethod() throws Exception {
public void preflightRequestWrongAllowedMethod() {
ServerWebExchange exchange = MockServerWebExchange.from(
preFlightRequest().header(ACCESS_CONTROL_REQUEST_METHOD, "DELETE"));
this.conf.addAllowedOrigin("*");
@@ -220,7 +225,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestMatchedAllowedMethod() throws Exception {
public void preflightRequestMatchedAllowedMethod() {
ServerWebExchange exchange = MockServerWebExchange.from(
preFlightRequest().header(ACCESS_CONTROL_REQUEST_METHOD, "GET"));
this.conf.addAllowedOrigin("*");
@@ -234,7 +239,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestTestWithOriginButWithoutOtherHeaders() throws Exception {
public void preflightRequestTestWithOriginButWithoutOtherHeaders() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest());
this.processor.process(this.conf, exchange);
@@ -246,7 +251,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestWithoutRequestMethod() throws Exception {
public void preflightRequestWithoutRequestMethod() {
ServerWebExchange exchange = MockServerWebExchange.from(
preFlightRequest().header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1"));
this.processor.process(this.conf, exchange);
@@ -259,7 +264,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestWithRequestAndMethodHeaderButNoConfig() throws Exception {
public void preflightRequestWithRequestAndMethodHeaderButNoConfig() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1"));
@@ -274,7 +279,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestValidRequestAndConfig() throws Exception {
public void preflightRequestValidRequestAndConfig() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1"));
@@ -299,7 +304,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestCredentials() throws Exception {
public void preflightRequestCredentials() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1"));
@@ -323,7 +328,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestCredentialsWithOriginWildcard() throws Exception {
public void preflightRequestCredentialsWithWildcardOrigin() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1"));
@@ -333,7 +338,10 @@ public class DefaultCorsProcessorTests {
this.conf.addAllowedOrigin("http://domain3.example");
this.conf.addAllowedHeader("Header1");
this.conf.setAllowCredentials(true);
assertThatIllegalArgumentException().isThrownBy(() -> this.processor.process(this.conf, exchange));
this.conf.setAllowedOrigins(null);
this.conf.addAllowedOriginPattern("*");
this.processor.process(this.conf, exchange);
ServerHttpResponse response = exchange.getResponse();
@@ -345,7 +353,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestAllowedHeaders() throws Exception {
public void preflightRequestAllowedHeaders() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1, Header2"));
@@ -369,7 +377,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestAllowsAllHeaders() throws Exception {
public void preflightRequestAllowsAllHeaders() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1, Header2"));
@@ -391,7 +399,7 @@ public class DefaultCorsProcessorTests {
}
@Test
public void preflightRequestWithEmptyHeaders() throws Exception {
public void preflightRequestWithEmptyHeaders() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(ACCESS_CONTROL_REQUEST_HEADERS, ""));