Skip to content

Commit cd9b390

Browse files
committed
Introduce CorsFilter and CorsConfigurationMapping
This commit introduces the following changes: - The new CorsConfigurationMapping class allows to share the mapped CorsConfiguration logic between AbstractHandlerMapping and CorsFilter - In AbstractHandlerMapping, the Map<String, CorsConfiguration> corsConfiguration property has been renamed to corsConfigurations - CorsFilter allows to process CORS requests at filter level, using any CorsConfigurationSource implementation (for example CorsConfigurationMapping) Issue: SPR-13192
1 parent df9290c commit cd9b390

File tree

13 files changed

+429
-50
lines changed

13 files changed

+429
-50
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright 2002-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.cors;
18+
19+
import java.util.Collections;
20+
import java.util.LinkedHashMap;
21+
import java.util.Map;
22+
import javax.servlet.http.HttpServletRequest;
23+
24+
import org.springframework.util.AntPathMatcher;
25+
import org.springframework.util.Assert;
26+
import org.springframework.util.PathMatcher;
27+
import org.springframework.web.util.UrlPathHelper;
28+
29+
/**
30+
* Provide a per request {@link CorsConfiguration} instance based on a collection of
31+
* {@link CorsConfiguration} mapped on path patterns.
32+
*
33+
* <p>Exact path mapping URIs (such as {@code "/admin"}) are supported as
34+
* well as Ant-style path patterns (such as {@code "/admin/**"}).
35+
*
36+
* @author Sebastien Deleuze
37+
* @since 4.2
38+
*/
39+
public class CorsConfigurationMapping implements CorsConfigurationSource {
40+
41+
private final Map<String, CorsConfiguration> corsConfigurations =
42+
new LinkedHashMap<String, CorsConfiguration>();
43+
44+
private PathMatcher pathMatcher = new AntPathMatcher();
45+
46+
private UrlPathHelper urlPathHelper = new UrlPathHelper();
47+
48+
49+
/**
50+
* Set the PathMatcher implementation to use for matching URL paths
51+
* against registered URL patterns. Default is AntPathMatcher.
52+
* @see org.springframework.util.AntPathMatcher
53+
*/
54+
public void setPathMatcher(PathMatcher pathMatcher) {
55+
Assert.notNull(pathMatcher, "PathMatcher must not be null");
56+
this.pathMatcher = pathMatcher;
57+
}
58+
59+
/**
60+
* Set if URL lookup should always use the full path within the current servlet
61+
* context. Else, the path within the current servlet mapping is used if applicable
62+
* (that is, in the case of a ".../*" servlet mapping in web.xml).
63+
* <p>Default is "false".
64+
* @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
65+
*/
66+
public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
67+
this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
68+
}
69+
70+
/**
71+
* Set if context path and request URI should be URL-decoded. Both are returned
72+
* <i>undecoded</i> by the Servlet API, in contrast to the servlet path.
73+
* <p>Uses either the request encoding or the default encoding according
74+
* to the Servlet spec (ISO-8859-1).
75+
* @see org.springframework.web.util.UrlPathHelper#setUrlDecode
76+
*/
77+
public void setUrlDecode(boolean urlDecode) {
78+
this.urlPathHelper.setUrlDecode(urlDecode);
79+
}
80+
81+
/**
82+
* Set if ";" (semicolon) content should be stripped from the request URI.
83+
* <p>The default value is {@code true}.
84+
* @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)
85+
*/
86+
public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
87+
this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
88+
}
89+
90+
/**
91+
* Set the UrlPathHelper to use for resolution of lookup paths.
92+
* <p>Use this to override the default UrlPathHelper with a custom subclass.
93+
*/
94+
public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
95+
Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
96+
this.urlPathHelper = urlPathHelper;
97+
}
98+
99+
/**
100+
* Set CORS configuration based on URL patterns.
101+
*/
102+
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
103+
this.corsConfigurations.clear();
104+
if (corsConfigurations != null) {
105+
this.corsConfigurations.putAll(corsConfigurations);
106+
}
107+
}
108+
109+
/**
110+
* Get the CORS configuration.
111+
*/
112+
public Map<String, CorsConfiguration> getCorsConfigurations() {
113+
return Collections.unmodifiableMap(this.corsConfigurations);
114+
}
115+
116+
/**
117+
* Register a {@link CorsConfiguration} for the specified path pattern.
118+
*/
119+
public void registerCorsConfiguration(String path, CorsConfiguration config) {
120+
this.corsConfigurations.put(path, config);
121+
}
122+
123+
@Override
124+
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
125+
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
126+
for(Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
127+
if (this.pathMatcher.match(entry.getKey(), lookupPath)) {
128+
return entry.getValue();
129+
}
130+
}
131+
return null;
132+
}
133+
134+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2002-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.filter;
18+
19+
import java.io.IOException;
20+
import javax.servlet.FilterChain;
21+
import javax.servlet.ServletException;
22+
import javax.servlet.http.HttpServletRequest;
23+
import javax.servlet.http.HttpServletResponse;
24+
25+
import org.springframework.util.Assert;
26+
import org.springframework.web.cors.CorsConfiguration;
27+
import org.springframework.web.cors.CorsConfigurationMapping;
28+
import org.springframework.web.cors.CorsConfigurationSource;
29+
import org.springframework.web.cors.CorsProcessor;
30+
import org.springframework.web.cors.CorsUtils;
31+
import org.springframework.web.cors.DefaultCorsProcessor;
32+
33+
/**
34+
* {@link javax.servlet.Filter} that handles CORS preflight requests and intercepts CORS
35+
* simple and actual requests thanks to a {@link CorsProcessor} implementation
36+
* ({@link DefaultCorsProcessor} by default) in order to add the relevant CORS response
37+
* headers (like {@code Access-Control-Allow-Origin}) using the provided
38+
* {@link CorsConfigurationSource} (for example a {@link CorsConfigurationMapping} instance.
39+
*
40+
* <p>This filter could be used in conjunction with {@link DelegatingFilterProxy} in order
41+
* to help with its initialization.
42+
*
43+
* @author Sebastien Deleuze
44+
* @since 4.2
45+
* @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a>
46+
*/
47+
public class CorsFilter extends OncePerRequestFilter {
48+
49+
private CorsProcessor processor = new DefaultCorsProcessor();
50+
51+
private final CorsConfigurationSource source;
52+
53+
54+
/**
55+
* Constructor accepting a {@link CorsConfigurationSource}, this source will be used
56+
* by the filter to find the {@link CorsConfiguration} to use for each incoming request.
57+
* @see CorsConfigurationMapping
58+
*/
59+
public CorsFilter(CorsConfigurationSource source) {
60+
this.source = source;
61+
}
62+
63+
/**
64+
* Configure a custom {@link CorsProcessor} to use to apply the matched
65+
* {@link CorsConfiguration} for a request.
66+
* <p>By default {@link DefaultCorsProcessor} is used.
67+
*/
68+
public void setCorsProcessor(CorsProcessor processor) {
69+
Assert.notNull(processor, "CorsProcessor must not be null");
70+
this.processor = processor;
71+
}
72+
73+
@Override
74+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
75+
FilterChain filterChain) throws ServletException, IOException {
76+
77+
if (CorsUtils.isCorsRequest(request)) {
78+
CorsConfiguration corsConfiguration = this.source.getCorsConfiguration(request);
79+
if (corsConfiguration != null) {
80+
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
81+
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
82+
return;
83+
}
84+
}
85+
}
86+
filterChain.doFilter(request, response);
87+
}
88+
89+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2002-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.cors;
18+
19+
import static org.junit.Assert.*;
20+
import org.junit.Test;
21+
22+
import org.springframework.http.HttpMethod;
23+
import org.springframework.mock.web.test.MockHttpServletRequest;
24+
25+
/**
26+
* Unit tests for {@link CorsConfigurationMapping}.
27+
* @author Sebastien Deleuze
28+
*/
29+
public class CorsConfigurationMappingTests {
30+
31+
private final CorsConfigurationMapping mapping = new CorsConfigurationMapping();
32+
33+
@Test
34+
public void empty() {
35+
assertNull(this.mapping.getCorsConfiguration(new MockHttpServletRequest(HttpMethod.GET.name(), "/bar/test.html")));
36+
}
37+
38+
@Test
39+
public void registerAndMatch() {
40+
CorsConfiguration config = new CorsConfiguration();
41+
this.mapping.registerCorsConfiguration("/bar/**", config);
42+
assertNull(this.mapping.getCorsConfiguration(new MockHttpServletRequest(HttpMethod.GET.name(), "/foo/test.html")));
43+
assertEquals(config, this.mapping.getCorsConfiguration(new MockHttpServletRequest(HttpMethod.GET.name(), "/bar/test.html")));
44+
}
45+
46+
@Test(expected = UnsupportedOperationException.class)
47+
public void unmodifiableConfigurationsMap() {
48+
this.mapping.getCorsConfigurations().put("/**", new CorsConfiguration());
49+
}
50+
51+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2002-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.filter;
18+
19+
import java.io.IOException;
20+
import java.util.Arrays;
21+
import javax.servlet.FilterChain;
22+
import javax.servlet.ServletException;
23+
24+
import static org.junit.Assert.*;
25+
import org.junit.Before;
26+
import org.junit.Test;
27+
28+
import org.springframework.http.HttpHeaders;
29+
import org.springframework.http.HttpMethod;
30+
import org.springframework.mock.web.test.MockHttpServletRequest;
31+
import org.springframework.mock.web.test.MockHttpServletResponse;
32+
import org.springframework.web.cors.CorsConfiguration;
33+
34+
/**
35+
* Unit tests for {@link CorsFilter}.
36+
* @author Sebastien Deleuze
37+
*/
38+
public class CorsFilterTests {
39+
40+
private CorsFilter filter;
41+
42+
private final CorsConfiguration config = new CorsConfiguration();
43+
44+
@Before
45+
public void setup() throws Exception {
46+
config.setAllowedOrigins(Arrays.asList("http://domain1.com", "http://domain2.com"));
47+
config.setAllowedMethods(Arrays.asList("GET", "POST"));
48+
config.setAllowedHeaders(Arrays.asList("header1", "header2"));
49+
config.setExposedHeaders(Arrays.asList("header3", "header4"));
50+
config.setMaxAge(123L);
51+
config.setAllowCredentials(false);
52+
filter = new CorsFilter(r -> config);
53+
}
54+
55+
@Test
56+
public void validActualRequest() throws ServletException, IOException {
57+
58+
MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/test.html");
59+
request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com");
60+
request.addHeader("header2", "foo");
61+
MockHttpServletResponse response = new MockHttpServletResponse();
62+
63+
FilterChain filterChain = (filterRequest, filterResponse) -> {
64+
assertEquals("http://domain2.com", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN));
65+
assertEquals("header3, header4", response.getHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS));
66+
};
67+
filter.doFilter(request, response, filterChain);
68+
}
69+
70+
@Test
71+
public void invalidActualRequest() throws ServletException, IOException {
72+
73+
MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.DELETE.name(), "/test.html");
74+
request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com");
75+
request.addHeader("header2", "foo");
76+
MockHttpServletResponse response = new MockHttpServletResponse();
77+
78+
FilterChain filterChain = (filterRequest, filterResponse) -> {
79+
fail("Invalid requests must not be forwarded to the filter chain");
80+
};
81+
filter.doFilter(request, response, filterChain);
82+
assertNull(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN));
83+
}
84+
85+
@Test
86+
public void validPreFlightRequest() throws ServletException, IOException {
87+
88+
MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.OPTIONS.name(), "/test.html");
89+
request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com");
90+
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.GET.name());
91+
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "header1, header2");
92+
MockHttpServletResponse response = new MockHttpServletResponse();
93+
94+
FilterChain filterChain = (filterRequest, filterResponse) ->
95+
fail("Preflight requests must not be forwarded to the filter chain");
96+
filter.doFilter(request, response, filterChain);
97+
98+
assertEquals("http://domain2.com", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN));
99+
assertEquals("header1, header2", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS));
100+
assertEquals("header3, header4", response.getHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS));
101+
assertEquals(123L, Long.parseLong(response.getHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE)));
102+
}
103+
104+
@Test
105+
public void invalidPreFlightRequest() throws ServletException, IOException {
106+
107+
MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.OPTIONS.name(), "/test.html");
108+
request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com");
109+
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.DELETE.name());
110+
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "header1, header2");
111+
MockHttpServletResponse response = new MockHttpServletResponse();
112+
113+
FilterChain filterChain = (filterRequest, filterResponse) ->
114+
fail("Preflight requests must not be forwarded to the filter chain");
115+
filter.doFilter(request, response, filterChain);
116+
117+
assertNull(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN));
118+
}
119+
120+
}

0 commit comments

Comments
 (0)