Skip to content

Commit 49fff75

Browse files
committed
Add global CORS configuration capabilities
This commit adds JavaConfig based global CORS configuration capabilities to Spring MVC. It is now possible to specify multiple CORS configurations, each mapped on a path pattern, by overriding WebMvcConfigurerAdapter#configureCrossOrigin(CrossOriginConfigurer). It is also possible to combine global and @crossorigin based CORS configuration. Issue: SPR-12933
1 parent 696a010 commit 49fff75

File tree

14 files changed

+548
-13
lines changed

14 files changed

+548
-13
lines changed

spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,55 @@ public class CorsConfiguration {
5353
public CorsConfiguration() {
5454
}
5555

56+
/**
57+
* Copy constructor.
58+
*/
59+
public CorsConfiguration(CorsConfiguration other) {
60+
this.allowedOrigins = other.allowedOrigins;
61+
this.allowedMethods = other.allowedMethods;
62+
this.allowedHeaders = other.allowedHeaders;
63+
this.exposedHeaders = other.exposedHeaders;
64+
this.allowCredentials = other.allowCredentials;
65+
this.maxAge = other.maxAge;
66+
}
67+
68+
/**
69+
* Combine the specified {@link CorsConfiguration} with this one.
70+
* Properties of this configuration are overridden only by non-null properties
71+
* of the provided one.
72+
* @return the combined {@link CorsConfiguration}
73+
*/
74+
public CorsConfiguration combine(CorsConfiguration other) {
75+
if (other == null) {
76+
return this;
77+
}
78+
CorsConfiguration config = new CorsConfiguration(this);
79+
config.setAllowedOrigins(combine(this.getAllowedOrigins(), other.getAllowedOrigins()));
80+
config.setAllowedMethods(combine(this.getAllowedMethods(), other.getAllowedMethods()));
81+
config.setAllowedHeaders(combine(this.getAllowedHeaders(), other.getAllowedHeaders()));
82+
config.setExposedHeaders(combine(this.getExposedHeaders(), other.getExposedHeaders()));
83+
Boolean allowCredentials = other.getAllowCredentials();
84+
if (allowCredentials != null) {
85+
config.setAllowCredentials(allowCredentials);
86+
}
87+
Long maxAge = other.getMaxAge();
88+
if (maxAge != null) {
89+
config.setMaxAge(maxAge);
90+
}
91+
return config;
92+
}
93+
94+
private List<String> combine(List<String> source, List<String> other) {
95+
if (other == null) {
96+
return source;
97+
}
98+
if (source == null || source.contains("*")) {
99+
return other;
100+
}
101+
List<String> combined = new ArrayList<String>(source);
102+
combined.addAll(other);
103+
return combined;
104+
}
56105

57106
/**
58107
* Configure origins to allow, e.g. "http://domain1.com". The special value
@@ -142,13 +191,19 @@ public List<String> getAllowedHeaders() {
142191
* <p>By default this is not set.
143192
*/
144193
public void setExposedHeaders(List<String> exposedHeaders) {
194+
if (exposedHeaders != null && exposedHeaders.contains("*")) {
195+
throw new IllegalArgumentException("'*' is not a valid exposed header value");
196+
}
145197
this.exposedHeaders = exposedHeaders;
146198
}
147199

148200
/**
149201
* Add a single response header to expose.
150202
*/
151203
public void addExposedHeader(String exposedHeader) {
204+
if ("*".equals(exposedHeader)) {
205+
throw new IllegalArgumentException("'*' is not a valid exposed header value");
206+
}
152207
if (this.exposedHeaders == null) {
153208
this.exposedHeaders = new ArrayList<String>();
154209
}

spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
import java.util.Arrays;
2121
import java.util.Collections;
2222

23-
import static org.junit.Assert.assertEquals;
24-
import static org.junit.Assert.assertNull;
23+
import static org.junit.Assert.*;
2524
import org.junit.Before;
2625
import org.junit.Test;
2726

@@ -40,6 +39,115 @@ public class CorsConfigurationTests {
4039
public void setup() {
4140
config = new CorsConfiguration();
4241
}
42+
43+
@Test
44+
public void setNullValues() {
45+
config.setAllowedOrigins(null);
46+
assertNull(config.getAllowedOrigins());
47+
config.setAllowedHeaders(null);
48+
assertNull(config.getAllowedHeaders());
49+
config.setAllowedMethods(null);
50+
assertNull(config.getAllowedMethods());
51+
config.setExposedHeaders(null);
52+
assertNull(config.getExposedHeaders());
53+
config.setAllowCredentials(null);
54+
assertNull(config.getAllowCredentials());
55+
config.setMaxAge(null);
56+
assertNull(config.getMaxAge());
57+
}
58+
59+
@Test
60+
public void setValues() {
61+
config.addAllowedOrigin("*");
62+
assertEquals(Arrays.asList("*"), config.getAllowedOrigins());
63+
config.addAllowedHeader("*");
64+
assertEquals(Arrays.asList("*"), config.getAllowedHeaders());
65+
config.addAllowedMethod("*");
66+
assertEquals(Arrays.asList("*"), config.getAllowedMethods());
67+
config.addExposedHeader("header1");
68+
config.addExposedHeader("header2");
69+
assertEquals(Arrays.asList("header1", "header2"), config.getExposedHeaders());
70+
config.setAllowCredentials(true);
71+
assertTrue(config.getAllowCredentials());
72+
config.setMaxAge(123L);
73+
assertEquals(new Long(123), config.getMaxAge());
74+
}
75+
76+
@Test(expected = IllegalArgumentException.class)
77+
public void asteriskWildCardOnAddExposedHeader() {
78+
config.addExposedHeader("*");
79+
}
80+
81+
@Test(expected = IllegalArgumentException.class)
82+
public void asteriskWildCardOnSetExposedHeaders() {
83+
config.setExposedHeaders(Arrays.asList("*"));
84+
}
85+
86+
@Test
87+
public void combineWithNull() {
88+
config.setAllowedOrigins(Arrays.asList("*"));
89+
config.combine(null);
90+
assertEquals(Arrays.asList("*"), config.getAllowedOrigins());
91+
}
92+
93+
@Test
94+
public void combineWithNullProperties() {
95+
config.addAllowedOrigin("*");
96+
config.addAllowedHeader("header1");
97+
config.addExposedHeader("header3");
98+
config.addAllowedMethod(HttpMethod.GET.name());
99+
config.setMaxAge(123L);
100+
config.setAllowCredentials(true);
101+
CorsConfiguration other = new CorsConfiguration();
102+
config = config.combine(other);
103+
assertEquals(Arrays.asList("*"), config.getAllowedOrigins());
104+
assertEquals(Arrays.asList("header1"), config.getAllowedHeaders());
105+
assertEquals(Arrays.asList("header3"), config.getExposedHeaders());
106+
assertEquals(Arrays.asList(HttpMethod.GET.name()), config.getAllowedMethods());
107+
assertEquals(new Long(123), config.getMaxAge());
108+
assertTrue(config.getAllowCredentials());
109+
}
110+
111+
@Test
112+
public void combineWithAsteriskWildCard() {
113+
config.addAllowedOrigin("*");
114+
config.addAllowedHeader("*");
115+
config.addAllowedMethod("*");
116+
CorsConfiguration other = new CorsConfiguration();
117+
other.addAllowedOrigin("http://domain.com");
118+
other.addAllowedHeader("header1");
119+
other.addExposedHeader("header2");
120+
other.addAllowedMethod(HttpMethod.PUT.name());
121+
config = config.combine(other);
122+
assertEquals(Arrays.asList("http://domain.com"), config.getAllowedOrigins());
123+
assertEquals(Arrays.asList("header1"), config.getAllowedHeaders());
124+
assertEquals(Arrays.asList("header2"), config.getExposedHeaders());
125+
assertEquals(Arrays.asList(HttpMethod.PUT.name()), config.getAllowedMethods());
126+
}
127+
128+
@Test
129+
public void combine() {
130+
config.addAllowedOrigin("http://domain1.com");
131+
config.addAllowedHeader("header1");
132+
config.addExposedHeader("header3");
133+
config.addAllowedMethod(HttpMethod.GET.name());
134+
config.setMaxAge(123L);
135+
config.setAllowCredentials(true);
136+
CorsConfiguration other = new CorsConfiguration();
137+
other.addAllowedOrigin("http://domain2.com");
138+
other.addAllowedHeader("header2");
139+
other.addExposedHeader("header4");
140+
other.addAllowedMethod(HttpMethod.PUT.name());
141+
other.setMaxAge(456L);
142+
other.setAllowCredentials(false);
143+
config = config.combine(other);
144+
assertEquals(Arrays.asList("http://domain1.com", "http://domain2.com"), config.getAllowedOrigins());
145+
assertEquals(Arrays.asList("header1", "header2"), config.getAllowedHeaders());
146+
assertEquals(Arrays.asList("header3", "header4"), config.getExposedHeaders());
147+
assertEquals(Arrays.asList(HttpMethod.GET.name(), HttpMethod.PUT.name()), config.getAllowedMethods());
148+
assertEquals(new Long(456), config.getMaxAge());
149+
assertFalse(config.getAllowCredentials());
150+
}
43151

44152
@Test
45153
public void checkOriginAllowed() {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.servlet.config.annotation;
18+
19+
import java.util.ArrayList;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
import org.springframework.web.cors.CorsConfiguration;
25+
26+
/**
27+
* Assist with the registration of {@link CorsConfiguration} mapped to one or more path patterns.
28+
* @author Sebastien Deleuze
29+
*
30+
* @since 4.2
31+
* @see CrossOriginRegistration
32+
*/
33+
public class CrossOriginConfigurer {
34+
35+
private final List<CrossOriginRegistration> registrations = new ArrayList<CrossOriginRegistration>();
36+
37+
/**
38+
* Enable cross origin requests on the specified path patterns. If no path pattern is specified,
39+
* cross-origin request handling is mapped on "/**" .
40+
*
41+
* <p>By default, all origins, all headers and credentials are allowed. Max age is set to 30 minutes.</p>
42+
*/
43+
public CrossOriginRegistration enableCrossOrigin(String... pathPatterns) {
44+
CrossOriginRegistration registration = new CrossOriginRegistration(pathPatterns);
45+
this.registrations.add(registration);
46+
return registration;
47+
}
48+
49+
protected Map<String, CorsConfiguration> getCorsConfigurations() {
50+
Map<String, CorsConfiguration> configs = new HashMap<String, CorsConfiguration>();
51+
for (CrossOriginRegistration registration : this.registrations) {
52+
for (String pathPattern : registration.getPathPatterns()) {
53+
configs.put(pathPattern, registration.getCorsConfiguration());
54+
}
55+
}
56+
return configs;
57+
}
58+
59+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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.servlet.config.annotation;
18+
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
22+
import org.springframework.http.HttpMethod;
23+
import org.springframework.web.cors.CorsConfiguration;
24+
25+
/**
26+
* Assists with the creation of a {@link CorsConfiguration} mapped to one or more path patterns.
27+
* If no path pattern is specified, cross-origin request handling is mapped on "/**" .
28+
*
29+
* <p>By default, all origins, all headers, credentials and GET, HEAD, POST methods are allowed.
30+
* Max age is set to 30 minutes.</p>
31+
*
32+
* @author Sebastien Deleuze
33+
* @since 4.2
34+
*/
35+
public class CrossOriginRegistration {
36+
37+
private final String[] pathPatterns;
38+
39+
private final CorsConfiguration config;
40+
41+
public CrossOriginRegistration(String... pathPatterns) {
42+
this.pathPatterns = (pathPatterns.length == 0 ? new String[]{ "/**" } : pathPatterns);
43+
// Same default values than @CrossOrigin annotation + allows simple methods
44+
this.config = new CorsConfiguration();
45+
this.config.addAllowedOrigin("*");
46+
this.config.addAllowedMethod(HttpMethod.GET.name());
47+
this.config.addAllowedMethod(HttpMethod.HEAD.name());
48+
this.config.addAllowedMethod(HttpMethod.POST.name());
49+
this.config.addAllowedHeader("*");
50+
this.config.setAllowCredentials(true);
51+
this.config.setMaxAge(1800L);
52+
}
53+
54+
public CrossOriginRegistration allowedOrigins(String... origins) {
55+
this.config.setAllowedOrigins(new ArrayList<String>(Arrays.asList(origins)));
56+
return this;
57+
}
58+
59+
public CrossOriginRegistration allowedMethods(String... methods) {
60+
this.config.setAllowedMethods(new ArrayList<String>(Arrays.asList(methods)));
61+
return this;
62+
}
63+
64+
public CrossOriginRegistration allowedHeaders(String... headers) {
65+
this.config.setAllowedHeaders(new ArrayList<String>(Arrays.asList(headers)));
66+
return this;
67+
}
68+
69+
public CrossOriginRegistration exposedHeaders(String... headers) {
70+
this.config.setExposedHeaders(new ArrayList<String>(Arrays.asList(headers)));
71+
return this;
72+
}
73+
74+
public CrossOriginRegistration maxAge(long maxAge) {
75+
this.config.setMaxAge(maxAge);
76+
return this;
77+
}
78+
79+
public CrossOriginRegistration allowCredentials(boolean allowCredentials) {
80+
this.config.setAllowCredentials(allowCredentials);
81+
return this;
82+
}
83+
84+
protected String[] getPathPatterns() {
85+
return this.pathPatterns;
86+
}
87+
88+
protected CorsConfiguration getCorsConfiguration() {
89+
return this.config;
90+
}
91+
92+
}

spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,9 @@ protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver>
132132
this.configurers.configureHandlerExceptionResolvers(exceptionResolvers);
133133
}
134134

135+
@Override
136+
protected void configureCrossOrigin(CrossOriginConfigurer configurer) {
137+
this.configurers.configureCrossOrigin(configurer);
138+
}
139+
135140
}

0 commit comments

Comments
 (0)