Skip to content

Commit a3870ca

Browse files
mp911deodrotbohm
authored andcommitted
DATAREST-573 - Add support for new CORS configuration mechanisms introduced in Spring 4.2.
We now support CORS configuration mechanisms introduced in Spring 4.2. CORS can be configured on multiple levels: Repository interface, Repository REST controller and global level. Spring Data REST CORS configuration is isolated so Spring Web MVC'S CORS configuration does not apply to Spring Data REST resources. Multiple configuration sources are merged so different aspects of CORS can be configured in separate locations. @crossorigin interface PersonRepository extends CrudRepository<Person, Long> {} @RepositoryRestController @RequestMapping("/person") public class PersonController { @crossorigin(maxAge = 3600) @RequestMapping(method = RequestMethod.GET, "/xml/{id}", produces = MediaType.APPLICATION_XML_VALUE) public Person retrieve(@PathVariable Long id) { // ... } } @component public class SpringDataRestCustomization extends RepositoryRestConfigurerAdapter { @OverRide public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { config.addCorsMapping("/person/**") .allowedOrigins("http://domain2.com") .allowedMethods("PUT", "DELETE") .allowedHeaders("header1", "header2", "header3") .exposedHeaders("header1", "header2") .allowCredentials(false).maxAge(3600); } }
1 parent 19aa419 commit a3870ca

File tree

11 files changed

+698
-10
lines changed

11 files changed

+698
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2016 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+
package org.springframework.data.rest.core.config;
17+
18+
import java.util.Map;
19+
20+
import org.springframework.web.cors.CorsConfiguration;
21+
import org.springframework.web.servlet.config.annotation.CorsRegistry;
22+
23+
/**
24+
* Spring Data REST specific {@code CorsRegistry} implementation exposing {@link #getCorsConfigurations()}. Assists with
25+
* the registration of {@link CorsConfiguration} mapped to a path pattern.
26+
*
27+
* @author Mark Paluch
28+
* @since 2.6
29+
*/
30+
public class RepositoryCorsRegistry extends CorsRegistry {
31+
32+
/* (non-Javadoc)
33+
* @see org.springframework.web.servlet.config.annotation.CorsRegistry#getCorsConfigurations()
34+
*/
35+
@Override
36+
public Map<String, CorsConfiguration> getCorsConfigurations() {
37+
return super.getCorsConfigurations();
38+
}
39+
}

spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,8 @@
2828
import org.springframework.http.MediaType;
2929
import org.springframework.util.Assert;
3030
import org.springframework.util.StringUtils;
31+
import org.springframework.web.cors.CorsConfiguration;
32+
import org.springframework.web.servlet.config.annotation.CorsRegistration;
3133

3234
/**
3335
* Spring Data REST configuration options.
@@ -36,6 +38,7 @@
3638
* @author Oliver Gierke
3739
* @author Jeremy Rickard
3840
* @author Greg Turnquist
41+
* @author Mark Paluch
3942
*/
4043
@SuppressWarnings("deprecation")
4144
public class RepositoryRestConfiguration {
@@ -58,6 +61,7 @@ public class RepositoryRestConfiguration {
5861
private ResourceMappingConfiguration repoMappings = new ResourceMappingConfiguration();
5962
private RepositoryDetectionStrategy repositoryDetectionStrategy = RepositoryDetectionStrategies.DEFAULT;
6063

64+
private final RepositoryCorsRegistry corsRegistry = new RepositoryCorsRegistry();
6165
private final ProjectionDefinitionConfiguration projectionConfiguration;
6266
private final MetadataConfiguration metadataConfiguration;
6367
private final EntityLookupConfiguration entityLookupConfiguration;
@@ -549,6 +553,34 @@ public void setRepositoryDetectionStrategy(RepositoryDetectionStrategy repositor
549553
: repositoryDetectionStrategy;
550554
}
551555

556+
/**
557+
* Returns the {@link RepositoryCorsRegistry} to configure Cross-origin resource sharing.
558+
*
559+
* @return the {@link RepositoryCorsRegistry}.
560+
* @since 2.6
561+
* @see RepositoryCorsRegistry
562+
* @see CorsRegistration
563+
*/
564+
public RepositoryCorsRegistry getCorsRegistry() {
565+
return corsRegistry;
566+
}
567+
568+
/**
569+
* Configures Cross-origin resource sharing given a {@code path}.
570+
*
571+
* @param path path or path pattern, must not be {@literal null} or empty.
572+
* @return the {@link CorsRegistration} to build a CORS configuration.
573+
* @since 2.6
574+
* @see CorsConfiguration
575+
*/
576+
public CorsRegistration addCorsMapping(String path) {
577+
578+
Assert.notNull(path, "Path must not be null!");
579+
Assert.hasText(path, "Path must not be empty!");
580+
581+
return corsRegistry.addMapping(path);
582+
}
583+
552584
/**
553585
* Returns the {@link EntityLookupRegistrar} to create custom {@link EntityLookup} instances registered in the
554586
* configuration.

spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryRestConfigurationUnitTests.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
*/
1616
package org.springframework.data.rest.core;
1717

18-
import static org.hamcrest.CoreMatchers.*;
18+
import static org.hamcrest.Matchers.*;
1919
import static org.junit.Assert.*;
2020
import static org.mockito.Mockito.*;
2121

22+
import java.util.Map;
23+
2224
import org.junit.Before;
2325
import org.junit.Test;
2426
import org.springframework.data.rest.core.config.EnumTranslationConfiguration;
@@ -28,11 +30,13 @@
2830
import org.springframework.data.rest.core.domain.Profile;
2931
import org.springframework.data.rest.core.domain.ProfileRepository;
3032
import org.springframework.http.MediaType;
33+
import org.springframework.web.cors.CorsConfiguration;
3134

3235
/**
3336
* Unit tests for {@link RepositoryRestConfiguration}.
3437
*
3538
* @author Oliver Gierke
39+
* @author Mark Paluch
3640
* @soundtrack Adam F - Circles (Colors)
3741
*/
3842
public class RepositoryRestConfigurationUnitTests {
@@ -132,10 +136,25 @@ public void returnsBodyForCreateIfExplicitlyActivated() {
132136
* @see DATAREST-776
133137
*/
134138
@Test
135-
public void consideresDomainTypeOfValueRepositoryLookupTypes() {
139+
public void considersDomainTypeOfValueRepositoryLookupTypes() {
136140

137141
configuration.withEntityLookup().forLookupRepository(ProfileRepository.class);
138142

139143
assertThat(configuration.isLookupType(Profile.class), is(true));
140144
}
145+
146+
/**
147+
* @see DATAREST-573
148+
*/
149+
@Test
150+
public void configuresCorsProcessing() {
151+
152+
configuration.addCorsMapping("/hello").maxAge(1234);
153+
154+
Map<String, CorsConfiguration> corsConfigurations = configuration.getCorsRegistry().getCorsConfigurations();
155+
assertThat(corsConfigurations, hasKey("/hello"));
156+
157+
CorsConfiguration corsConfiguration = corsConfigurations.get("/hello");
158+
assertThat(corsConfiguration.getMaxAge(), is(1234L));
159+
}
141160
}

spring-data-rest-tests/spring-data-rest-tests-jpa/src/main/java/org/springframework/data/rest/webmvc/jpa/AuthorRepository.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
package org.springframework.data.rest.webmvc.jpa;
1717

1818
import org.springframework.data.repository.CrudRepository;
19+
import org.springframework.web.bind.annotation.CrossOrigin;
20+
import org.springframework.web.bind.annotation.RequestMethod;
1921

2022
/**
2123
* @author Oliver Gierke
24+
* @author Mark Paluch
2225
*/
23-
public interface AuthorRepository extends CrudRepository<Author, Long> {
24-
25-
}
26+
@CrossOrigin(origins = "http://not.so.far.away", allowCredentials = "true",
27+
methods = { RequestMethod.GET, RequestMethod.PATCH }, maxAge = 1234)
28+
public interface AuthorRepository extends CrudRepository<Author, Long> {}

spring-data-rest-tests/spring-data-rest-tests-jpa/src/main/java/org/springframework/data/rest/webmvc/jpa/ItemRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
package org.springframework.data.rest.webmvc.jpa;
1717

1818
import org.springframework.data.repository.CrudRepository;
19+
import org.springframework.web.bind.annotation.CrossOrigin;
1920

2021
/**
2122
* @author Greg Turnquist
2223
* @author Oliver Gierke
24+
* @author Mark Paluch
2325
* @see DATAREST-463
2426
*/
27+
@CrossOrigin
2528
public interface ItemRepository extends CrudRepository<Item, Long> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright 2016 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+
package org.springframework.data.rest.webmvc.jpa;
17+
18+
import static org.hamcrest.Matchers.containsString;
19+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
20+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
21+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
22+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
23+
24+
import org.junit.Test;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
27+
import org.springframework.data.rest.tests.AbstractWebIntegrationTests;
28+
import org.springframework.data.rest.webmvc.BasePathAwareController;
29+
import org.springframework.data.rest.webmvc.RepositoryRestController;
30+
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
31+
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
32+
import org.springframework.hateoas.Link;
33+
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.MediaType;
35+
import org.springframework.test.context.ContextConfiguration;
36+
import org.springframework.web.bind.annotation.CrossOrigin;
37+
import org.springframework.web.bind.annotation.GetMapping;
38+
import org.springframework.web.bind.annotation.PathVariable;
39+
import org.springframework.web.bind.annotation.RequestMapping;
40+
import org.springframework.web.bind.annotation.RequestMethod;
41+
42+
/**
43+
* Web integration tests specific to Cross-origin resource sharing.
44+
*
45+
* @author Mark Paluch
46+
* @soundtrack 2 Unlimited - No Limit
47+
*/
48+
@ContextConfiguration
49+
public class CorsIntegrationTests extends AbstractWebIntegrationTests {
50+
51+
static class CorsConfig extends JpaRepositoryConfig {
52+
53+
@Bean
54+
RepositoryRestConfigurer repositoryRestConfigurer() {
55+
56+
return new RepositoryRestConfigurerAdapter() {
57+
58+
@Override
59+
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
60+
61+
config.addCorsMapping("/books/**") //
62+
.allowedMethods("GET", "PUT", "POST") //
63+
.allowedOrigins("http://far.far.away");
64+
}
65+
};
66+
}
67+
}
68+
69+
/**
70+
* @see DATAREST-573
71+
*/
72+
@Test
73+
public void appliesSelectiveDefaultCorsConfiguration() throws Exception {
74+
75+
Link findItems = client.discoverUnique("items");
76+
77+
// Preflight request
78+
mvc.perform(options(findItems.expand().getHref()).header(HttpHeaders.ORIGIN, "http://far.far.away")
79+
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")) //
80+
.andExpect(status().isOk()) //
81+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://far.far.away")) //
82+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,TRACE"));
83+
}
84+
85+
/**
86+
* @see DATAREST-573
87+
*/
88+
@Test
89+
public void appliesGlobalCorsConfiguration() throws Exception {
90+
91+
Link findBooks = client.discoverUnique("books");
92+
93+
// Preflight request
94+
mvc.perform(options(findBooks.expand().getHref()).header(HttpHeaders.ORIGIN, "http://far.far.away")
95+
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) //
96+
.andExpect(status().isOk()) //
97+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://far.far.away")) //
98+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,PUT,POST"));
99+
100+
// CORS request
101+
mvc.perform(get(findBooks.expand().getHref()).header(HttpHeaders.ORIGIN, "http://far.far.away")) //
102+
.andExpect(status().isOk()) //
103+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://far.far.away"));
104+
}
105+
106+
/**
107+
* @see DATAREST-573
108+
* @see BooksXmlController
109+
*/
110+
@Test
111+
public void appliesCorsConfigurationOnCustomControllers() throws Exception {
112+
113+
// Preflight request
114+
mvc.perform(options("/books/xml/1234") //
115+
.header(HttpHeaders.ORIGIN, "http://far.far.away") //
116+
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) //
117+
.andExpect(status().isOk()) //
118+
.andExpect(header().longValue(HttpHeaders.ACCESS_CONTROL_MAX_AGE, 77123)) //
119+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://far.far.away")) //
120+
// See https://jira.spring.io/browse/SPR-14792
121+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, containsString("GET,PUT,POST")));
122+
123+
// CORS request
124+
mvc.perform(get("/books/xml/1234") //
125+
.header(HttpHeaders.ORIGIN, "http://far.far.away") //
126+
.accept(MediaType.APPLICATION_XML)) //
127+
.andExpect(status().isOk()) //
128+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://far.far.away"));
129+
}
130+
131+
/**
132+
* @see DATAREST-573
133+
* @see BooksPdfController
134+
*/
135+
@Test
136+
public void appliesCorsConfigurationOnCustomControllerMethod() throws Exception {
137+
138+
// Preflight request
139+
mvc.perform(options("/books/pdf/1234").header(HttpHeaders.ORIGIN, "http://far.far.away")
140+
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) //
141+
.andExpect(status().isOk()) //
142+
.andExpect(header().longValue(HttpHeaders.ACCESS_CONTROL_MAX_AGE, 4711)) //
143+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://far.far.away")) //
144+
// See https://jira.spring.io/browse/SPR-14792
145+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, containsString("GET,PUT,POST")));
146+
}
147+
148+
/**
149+
* @see DATAREST-573
150+
*/
151+
@Test
152+
public void appliesCorsConfigurationOnRepository() throws Exception {
153+
154+
Link authorsLink = client.discoverUnique("authors");
155+
156+
// Preflight request
157+
mvc.perform(options(authorsLink.expand().getHref()).header(HttpHeaders.ORIGIN, "http://not.so.far.away")
158+
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) //
159+
.andExpect(status().isOk()) //
160+
.andExpect(header().longValue(HttpHeaders.ACCESS_CONTROL_MAX_AGE, 1234)) //
161+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://not.so.far.away")) //
162+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")) //
163+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,PATCH"));
164+
}
165+
166+
/**
167+
* @see DATAREST-573
168+
*/
169+
@Test
170+
public void appliesCorsConfigurationOnRepositoryToCustomControllers() throws Exception {
171+
172+
// Preflight request
173+
mvc.perform(options("/authors/pdf/1234").header(HttpHeaders.ORIGIN, "http://not.so.far.away")
174+
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) //
175+
.andExpect(status().isOk()) //
176+
.andExpect(header().longValue(HttpHeaders.ACCESS_CONTROL_MAX_AGE, 1234)) //
177+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://not.so.far.away")) //
178+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")) //
179+
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,PATCH"));
180+
}
181+
182+
@RepositoryRestController
183+
static class AuthorsPdfController {
184+
185+
@RequestMapping(method = RequestMethod.GET, path = "/authors/pdf/1234", produces = MediaType.APPLICATION_PDF_VALUE)
186+
void authorToPdf() {}
187+
}
188+
189+
@RepositoryRestController
190+
static class BooksPdfController {
191+
192+
@RequestMapping(method = RequestMethod.GET, path = "/books/pdf/1234", produces = MediaType.APPLICATION_PDF_VALUE)
193+
@CrossOrigin(maxAge = 4711)
194+
void bookToPdf() {}
195+
}
196+
197+
@BasePathAwareController
198+
static class BooksXmlController {
199+
200+
@GetMapping(value = "/books/xml/{id}", produces = MediaType.APPLICATION_XML_VALUE)
201+
@CrossOrigin(maxAge = 77123)
202+
void bookToXml(@PathVariable String id) {}
203+
}
204+
}

0 commit comments

Comments
 (0)