Updates to CORS patterns contribution
Closes gh-25016
This commit is contained in:
@@ -60,6 +60,10 @@ public class CorsBeanDefinitionParser implements BeanDefinitionParser {
|
||||
String[] allowedOrigins = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-origins"), ",");
|
||||
config.setAllowedOrigins(Arrays.asList(allowedOrigins));
|
||||
}
|
||||
if (mapping.hasAttribute("allowed-origin-patterns")) {
|
||||
String[] patterns = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-origin-patterns"), ",");
|
||||
config.setAllowedOriginPatterns(Arrays.asList(patterns));
|
||||
}
|
||||
if (mapping.hasAttribute("allowed-methods")) {
|
||||
String[] allowedMethods = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-methods"), ",");
|
||||
config.setAllowedMethods(Arrays.asList(allowedMethods));
|
||||
@@ -78,7 +82,9 @@ public class CorsBeanDefinitionParser implements BeanDefinitionParser {
|
||||
if (mapping.hasAttribute("max-age")) {
|
||||
config.setMaxAge(Long.parseLong(mapping.getAttribute("max-age")));
|
||||
}
|
||||
corsConfigurations.put(mapping.getAttribute("path"), config.applyPermitDefaultValues());
|
||||
config.applyPermitDefaultValues();
|
||||
config.validateAllowCredentials();
|
||||
corsConfigurations.put(mapping.getAttribute("path"), config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.web.servlet.config.annotation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
|
||||
@@ -46,24 +47,27 @@ public class CorsRegistration {
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
public CorsRegistration allowedOrigins(String... origins) {
|
||||
this.config.setAllowedOrigins(Arrays.asList(origins));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative to {@link #allowCredentials} 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
|
||||
*/
|
||||
public CorsRegistration allowedOriginPatterns(String... patterns) {
|
||||
this.config.setAllowedOriginPatterns(Arrays.asList(patterns));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, etc.
|
||||
|
||||
@@ -516,6 +516,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
|
||||
CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
|
||||
config = (globalConfig != null ? globalConfig.combine(config) : config);
|
||||
}
|
||||
if (config != null) {
|
||||
config.validateAllowCredentials();
|
||||
}
|
||||
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
|
||||
private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration();
|
||||
|
||||
static {
|
||||
ALLOW_CORS_CONFIG.addAllowedOrigin("*");
|
||||
ALLOW_CORS_CONFIG.addAllowedOriginPattern("*");
|
||||
ALLOW_CORS_CONFIG.addAllowedMethod("*");
|
||||
ALLOW_CORS_CONFIG.addAllowedHeader("*");
|
||||
ALLOW_CORS_CONFIG.setAllowCredentials(true);
|
||||
@@ -630,9 +630,10 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
|
||||
addMappingName(name, handlerMethod);
|
||||
}
|
||||
|
||||
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
|
||||
if (corsConfig != null) {
|
||||
this.corsLookup.put(handlerMethod, corsConfig);
|
||||
CorsConfiguration config = initCorsConfiguration(handler, method, mapping);
|
||||
if (config != null) {
|
||||
config.validateAllowCredentials();
|
||||
this.corsLookup.put(handlerMethod, config);
|
||||
}
|
||||
|
||||
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
|
||||
|
||||
@@ -1346,6 +1346,15 @@
|
||||
Comma-separated list of origins to allow, e.g. "https://domain1.com, https://domain2.com".
|
||||
The special value "*" allows all domains (default).
|
||||
|
||||
For matching pre-flight and actual requests the "Access-Control-Allow-Origin"
|
||||
response header is set either to the matched domain value or to "*".
|
||||
Keep in mind however that the CORS spec does not allow "*" when allow-credentials
|
||||
is set to true and that is rejected as of 5.3. See allowed-origin-patterns for
|
||||
further options.
|
||||
|
||||
By default all origins are allowed unless allowed-origin-patterns is also set
|
||||
in which case allowed-origin-patterns is used instead.
|
||||
|
||||
Note that CORS checks use values from "Forwarded" (RFC 7239), "X-Forwarded-Host",
|
||||
"X-Forwarded-Port", and "X-Forwarded-Proto" headers, if present, in order to reflect
|
||||
the client-originated address. Consider using the ForwardedHeaderFilter in order to
|
||||
@@ -1354,6 +1363,20 @@
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="allowed-origin-patterns" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Alternative to allowed-origins that supports origins declared via patterns.
|
||||
In contrast to allowed-origins which does support the special value "*", this
|
||||
property allows more flexible patterns, e.g. "*.domain1.com". Furthermore it
|
||||
always sets the "Access-Control-Allow-Origin" response header to the matched
|
||||
origin and never to "*" nor to any other pattern and therefore can be used in
|
||||
combination with allowCredentials set to true.
|
||||
|
||||
By default this is not set.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="allowed-methods" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
|
||||
@@ -914,7 +914,7 @@ public class MvcNamespaceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCors() throws Exception {
|
||||
public void testCors() {
|
||||
loadBeanDefinitions("mvc-config-cors.xml");
|
||||
|
||||
String[] beanNames = appContext.getBeanNamesForType(AbstractHandlerMapping.class);
|
||||
@@ -930,6 +930,7 @@ public class MvcNamespaceTests {
|
||||
CorsConfiguration config = configs.get("/api/**");
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[]{"https://domain1.com", "https://domain2.com"});
|
||||
assertThat(config.getAllowedOriginPatterns().toArray()).isEqualTo(new String[]{"http://*.domain.com"});
|
||||
assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[]{"GET", "PUT"});
|
||||
assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[]{"header1", "header2", "header3"});
|
||||
assertThat(config.getExposedHeaders().toArray()).isEqualTo(new String[]{"header1", "header2"});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.web.servlet.config.annotation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -61,11 +62,19 @@ public class CorsRegistryTests {
|
||||
assertThat(configs.size()).isEqualTo(1);
|
||||
CorsConfiguration config = configs.get("/foo");
|
||||
assertThat(config.getAllowedOrigins()).isEqualTo(Arrays.asList("https://domain2.com", "https://domain2.com"));
|
||||
assertThat(config.getAllowedMethods()).isEqualTo(Arrays.asList("DELETE"));
|
||||
assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("DELETE"));
|
||||
assertThat(config.getAllowedHeaders()).isEqualTo(Arrays.asList("header1", "header2"));
|
||||
assertThat(config.getExposedHeaders()).isEqualTo(Arrays.asList("header3", "header4"));
|
||||
assertThat(config.getAllowCredentials()).isEqualTo(false);
|
||||
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(3600));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowCredentials() {
|
||||
this.registry.addMapping("/foo").allowCredentials(true);
|
||||
CorsConfiguration config = this.registry.getCorsConfigurations().get("/foo");
|
||||
assertThat(config.getAllowedOrigins())
|
||||
.as("Globally origins=\"*\" and allowCredentials=true should be possible")
|
||||
.containsExactly("*");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,14 +123,14 @@ class CorsAbstractHandlerMappingTests {
|
||||
@PathPatternsParameterizedTest
|
||||
void actualRequestWithMappedPatternCorsConfiguration(TestHandlerMapping mapping) throws Exception {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.addAllowedOriginPattern(".*\\.domain2\\.com");
|
||||
config.addAllowedOriginPattern("http://*.domain2.com");
|
||||
mapping.setCorsConfigurations(Collections.singletonMap("/foo", config));
|
||||
MockHttpServletRequest request = getCorsRequest("/foo");
|
||||
HandlerExecutionChain chain = mapping.getHandler(request);
|
||||
|
||||
assertThat(chain).isNotNull();
|
||||
assertThat(chain.getHandler()).isInstanceOf(SimpleHandler.class);
|
||||
assertThat(mapping.getRequiredCorsConfig().getAllowedOriginPatterns()).containsExactly(".*\\.domain2\\.com");
|
||||
assertThat(mapping.getRequiredCorsConfig().getAllowedOriginPatterns()).containsExactly("http://*.domain2.com");
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
@@ -158,7 +158,8 @@ class CorsAbstractHandlerMappingTests {
|
||||
|
||||
CorsConfiguration config = mapping.getRequiredCorsConfig();
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getAllowedOrigins()).containsExactly("*");
|
||||
assertThat(config.getAllowedOrigins()).isNull();
|
||||
assertThat(config.getAllowedOriginPatterns()).containsExactly("*");
|
||||
assertThat(config.getAllowCredentials()).isTrue();
|
||||
}
|
||||
|
||||
@@ -174,7 +175,8 @@ class CorsAbstractHandlerMappingTests {
|
||||
|
||||
CorsConfiguration config = mapping.getRequiredCorsConfig();
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getAllowedOrigins()).containsExactly("*");
|
||||
assertThat(config.getAllowedOrigins()).isNull();
|
||||
assertThat(config.getAllowedOriginPatterns()).containsExactly("*");
|
||||
assertThat(config.getAllowCredentials()).isTrue();
|
||||
}
|
||||
|
||||
@@ -283,7 +285,7 @@ class CorsAbstractHandlerMappingTests {
|
||||
@Override
|
||||
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.addAllowedOrigin("*");
|
||||
config.addAllowedOriginPattern("*");
|
||||
config.setAllowCredentials(true);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.springframework.web.util.ServletRequestPathUtils;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
@@ -72,7 +73,7 @@ class CrossOriginTests {
|
||||
StaticWebApplicationContext wac = new StaticWebApplicationContext();
|
||||
Properties props = new Properties();
|
||||
props.setProperty("myOrigin", "https://example.com");
|
||||
props.setProperty("myDomainPattern", ".*\\.example\\.com");
|
||||
props.setProperty("myDomainPattern", "http://*.example.com");
|
||||
wac.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("ps", props));
|
||||
wac.registerSingleton("ppc", PropertySourcesPlaceholderConfigurer.class);
|
||||
wac.refresh();
|
||||
@@ -206,7 +207,7 @@ class CrossOriginTests {
|
||||
CorsConfiguration config = getCorsConfiguration(chain, false);
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getAllowedOrigins()).isNull();
|
||||
assertThat(config.getAllowedOriginPatterns()).isEqualTo(Collections.singletonList(".*\\.example\\.com"));
|
||||
assertThat(config.getAllowedOriginPatterns()).isEqualTo(Collections.singletonList("http://*.example.com"));
|
||||
assertThat(config.getAllowCredentials()).isNull();
|
||||
}
|
||||
|
||||
@@ -218,16 +219,30 @@ class CrossOriginTests {
|
||||
CorsConfiguration config = getCorsConfiguration(chain, false);
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getAllowedOrigins()).isNull();
|
||||
assertThat(config.getAllowedOriginPatterns()).isEqualTo(Collections.singletonList(".*\\.example\\.com"));
|
||||
assertThat(config.getAllowedOriginPatterns()).isEqualTo(Collections.singletonList("http://*.example.com"));
|
||||
assertThat(config.getAllowCredentials()).isNull();
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
void bogusAllowCredentialsValue(TestRequestMappingInfoHandlerMapping mapping) {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
mapping.registerHandler(new MethodLevelControllerWithBogusAllowCredentialsValue()))
|
||||
.withMessageContaining("@CrossOrigin's allowCredentials")
|
||||
.withMessageContaining("current value is [bogus]");
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> mapping.registerHandler(new MethodLevelControllerWithBogusAllowCredentialsValue()))
|
||||
.withMessageContaining("@CrossOrigin's allowCredentials")
|
||||
.withMessageContaining("current value is [bogus]");
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
void allowCredentialsWithDefaultOrigin(TestRequestMappingInfoHandlerMapping mapping) {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> mapping.registerHandler(new CredentialsWithDefaultOriginController()))
|
||||
.withMessageContaining("When allowCredentials is true, allowedOrigins cannot contain");
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
void allowCredentialsWithWildcardOrigin(TestRequestMappingInfoHandlerMapping mapping) {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> mapping.registerHandler(new CredentialsWithWildcardOriginController()))
|
||||
.withMessageContaining("When allowCredentials is true, allowedOrigins cannot contain");
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
@@ -255,7 +270,8 @@ class CrossOriginTests {
|
||||
config = getCorsConfiguration(chain, false);
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getAllowedMethods()).containsExactly("GET");
|
||||
assertThat(config.getAllowedOrigins()).containsExactly("*");
|
||||
assertThat(config.getAllowedOrigins()).isNull();
|
||||
assertThat(config.getAllowedOriginPatterns()).containsExactly("*");
|
||||
assertThat(config.getAllowCredentials()).isTrue();
|
||||
}
|
||||
|
||||
@@ -313,7 +329,8 @@ class CrossOriginTests {
|
||||
CorsConfiguration config = getCorsConfiguration(chain, true);
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getAllowedMethods()).containsExactly("*");
|
||||
assertThat(config.getAllowedOrigins()).containsExactly("*");
|
||||
assertThat(config.getAllowedOrigins()).isNull();
|
||||
assertThat(config.getAllowedOriginPatterns()).containsExactly("*");
|
||||
assertThat(config.getAllowedHeaders()).containsExactly("*");
|
||||
assertThat(config.getAllowCredentials()).isTrue();
|
||||
assertThat(CollectionUtils.isEmpty(config.getExposedHeaders())).isTrue();
|
||||
@@ -330,7 +347,8 @@ class CrossOriginTests {
|
||||
CorsConfiguration config = getCorsConfiguration(chain, true);
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getAllowedMethods()).containsExactly("*");
|
||||
assertThat(config.getAllowedOrigins()).containsExactly("*");
|
||||
assertThat(config.getAllowedOrigins()).isNull();
|
||||
assertThat(config.getAllowedOriginPatterns()).containsExactly("*");
|
||||
assertThat(config.getAllowedHeaders()).containsExactly("*");
|
||||
assertThat(config.getAllowCredentials()).isTrue();
|
||||
assertThat(CollectionUtils.isEmpty(config.getExposedHeaders())).isTrue();
|
||||
@@ -433,7 +451,7 @@ class CrossOriginTests {
|
||||
public void customOriginDefinedViaPlaceholder() {
|
||||
}
|
||||
|
||||
@CrossOrigin(originPatterns = ".*\\.example\\.com")
|
||||
@CrossOrigin(originPatterns = "http://*.example.com")
|
||||
@RequestMapping("/customOriginPattern")
|
||||
public void customOriginPatternDefinedViaValueAttribute() {
|
||||
}
|
||||
@@ -469,11 +487,31 @@ class CrossOriginTests {
|
||||
public void bar() {
|
||||
}
|
||||
|
||||
@CrossOrigin(allowCredentials = "true")
|
||||
@CrossOrigin(originPatterns = "*", allowCredentials = "true")
|
||||
@RequestMapping(path = "/baz", method = RequestMethod.GET)
|
||||
public void baz() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
@CrossOrigin(allowCredentials = "true")
|
||||
private static class CredentialsWithDefaultOriginController {
|
||||
|
||||
@GetMapping(path = "/no-origin")
|
||||
public void noOrigin() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
@CrossOrigin(allowCredentials = "true")
|
||||
private static class CredentialsWithWildcardOriginController {
|
||||
|
||||
@GetMapping(path = "/no-origin")
|
||||
@CrossOrigin(origins = "*")
|
||||
public void wildcardOrigin() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -495,6 +533,8 @@ class CrossOriginTests {
|
||||
@RequestMapping(path = "/foo", method = RequestMethod.GET)
|
||||
public void foo() {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<mvc:cors>
|
||||
|
||||
<mvc:mapping path="/api/**" allowed-origins="https://domain1.com, https://domain2.com"
|
||||
allowed-origin-patterns="http://*.domain.com"
|
||||
allowed-methods="GET, PUT" allowed-headers="header1, header2, header3"
|
||||
exposed-headers="header1, header2" allow-credentials="false" max-age="123" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user