From 580ecddcc04247dc422a2d8f38559d8a6cb8425d Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 6 May 2015 00:38:16 +0300 Subject: [PATCH] INT-3714: Add HTTP CORS Support JIRA: https://jira.spring.io/browse/INT-3714 Docs and change Reactor dependency to `RELEASE` AMQP -> `1.5.0.M1` and rebase Doc Polishing --- build.gradle | 6 +- .../config/HttpInboundEndpointParser.java | 20 +- .../integration/http/inbound/CrossOrigin.java | 93 +++++++++ .../HttpRequestHandlingEndpointSupport.java | 18 +- ...tegrationRequestMappingHandlerMapping.java | 46 ++++- .../config/spring-integration-http-4.2.xsd | 87 ++++++++ .../http/inbound/CrossOriginTests-context.xml | 52 +++++ .../http/inbound/CrossOriginTests.java | 190 ++++++++++++++++++ src/reference/asciidoc/http.adoc | 55 ++++- src/reference/asciidoc/whats-new.adoc | 8 + 10 files changed, 561 insertions(+), 14 deletions(-) create mode 100644 spring-integration-http/src/main/java/org/springframework/integration/http/inbound/CrossOrigin.java create mode 100644 spring-integration-http/src/test/java/org/springframework/integration/http/inbound/CrossOriginTests-context.xml create mode 100644 spring-integration-http/src/test/java/org/springframework/integration/http/inbound/CrossOriginTests.java diff --git a/build.gradle b/build.gradle index 63f7f44787..3795c9ed32 100644 --- a/build.gradle +++ b/build.gradle @@ -113,8 +113,8 @@ subprojects { subproject -> openJpaVersion = '2.3.0' pahoMqttClientVersion = '1.0.2' postgresVersion = '9.1-901-1.jdbc4' - reactorVersion = '2.0.1.BUILD-SNAPSHOT' - reactorSpringVersion = '2.0.1.BUILD-SNAPSHOT' + reactorVersion = '2.0.1.RELEASE' + reactorSpringVersion = '2.0.1.RELEASE' romeToolsVersion = '1.5.0' saajApiVersion = '1.3.5' saajImplVersion = '1.3.23' @@ -123,7 +123,7 @@ subprojects { subproject -> tomcatVersion = "8.0.18" smack3Version = '3.2.1' smackVersion = '4.0.6' - springAmqpVersion = project.hasProperty('springAmqpVersion') ? project.springAmqpVersion : '1.5.0.BUILD-SNAPSHOT' + springAmqpVersion = project.hasProperty('springAmqpVersion') ? project.springAmqpVersion : '1.5.0.M1' springDataMongoVersion = '1.7.0.RELEASE' springDataRedisVersion = '1.5.0.RELEASE' springGemfireVersion = '1.6.0.RELEASE' diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java index aaf11db599..16acd26825 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -29,6 +29,7 @@ import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.http.inbound.CrossOrigin; import org.springframework.integration.http.inbound.HttpRequestHandlingController; import org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway; import org.springframework.integration.http.inbound.RequestMapping; @@ -172,9 +173,24 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse builder.addPropertyValue("headerMapper", headerMapperBuilder.getBeanDefinition()); } - BeanDefinition requestMappingDef = this.createRequestMapping(element); + BeanDefinition requestMappingDef = createRequestMapping(element); builder.addPropertyValue("requestMapping", requestMappingDef); + + Element crossOriginElement = DomUtils.getChildElementByTagName(element, "cross-origin"); + if (crossOriginElement != null) { + BeanDefinitionBuilder crossOriginBuilder = + BeanDefinitionBuilder.genericBeanDefinition(CrossOrigin.class); + String[] attributes = {"origin", "allowed-headers", "exposed-headers", "max-age", "method"}; + for (String crossOriginAttribute : attributes) { + IntegrationNamespaceUtils.setValueIfAttributeDefined(crossOriginBuilder, crossOriginElement, + crossOriginAttribute); + } + IntegrationNamespaceUtils.setValueIfAttributeDefined(crossOriginBuilder, crossOriginElement, + "allow-credentials", true); + builder.addPropertyValue("crossOrigin", crossOriginBuilder.getBeanDefinition()); + } + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-payload-type", "requestPayloadType"); BeanDefinition statusCodeExpressionDef = diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/CrossOrigin.java b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/CrossOrigin.java new file mode 100644 index 0000000000..e886d70ae9 --- /dev/null +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/CrossOrigin.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.http.inbound; + +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * The mapping to permit cross origin requests (CORS) for {@link HttpRequestHandlingEndpointSupport}. + * Provides direct mapping in terms of functionality compared to + * {@link org.springframework.web.bind.annotation.CrossOrigin}. + * + * @author Artem Bilan + * @since 4.2 + * @see org.springframework.web.bind.annotation.CrossOrigin + * @see IntegrationRequestMappingHandlerMapping + */ +public class CrossOrigin { + + private String[] origin = {"*"}; + + private String[] allowedHeaders = {"*"}; + + private String[] exposedHeaders = {}; + + private RequestMethod[] method = {}; + + private Boolean allowCredentials = true; + + private long maxAge = 1800; + + public void setOrigin(String... origin) { + this.origin = origin; + } + + public String[] getOrigin() { + return origin; + } + + public void setAllowedHeaders(String... allowedHeaders) { + this.allowedHeaders = allowedHeaders; + } + + public String[] getAllowedHeaders() { + return allowedHeaders; + } + + public void setExposedHeaders(String... exposedHeaders) { + this.exposedHeaders = exposedHeaders; + } + + public String[] getExposedHeaders() { + return exposedHeaders; + } + + public void setMethod(RequestMethod... method) { + this.method = method; + } + + public RequestMethod[] getMethod() { + return method; + } + + public void setAllowCredentials(Boolean allowCredentials) { + this.allowCredentials = allowCredentials; + } + + public Boolean getAllowCredentials() { + return allowCredentials; + } + + public void setMaxAge(long maxAge) { + this.maxAge = maxAge; + } + + public long getMaxAge() { + return maxAge; + } + +} diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/HttpRequestHandlingEndpointSupport.java b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/HttpRequestHandlingEndpointSupport.java index aba0588f1b..78cd3267a9 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/HttpRequestHandlingEndpointSupport.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/HttpRequestHandlingEndpointSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -126,6 +126,8 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa private volatile RequestMapping requestMapping = new RequestMapping(); + private volatile CrossOrigin crossOrigin; + private volatile Class requestPayloadType = null; private volatile boolean convertersMerged; @@ -283,6 +285,20 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa return requestMapping; } + + /** + * Set the {@link CrossOrigin} to permit cross origin requests for this endpoint. + * @param crossOrigin the CrossOrigin config. + * @since 4.2 + */ + public void setCrossOrigin(CrossOrigin crossOrigin) { + this.crossOrigin = crossOrigin; + } + + public CrossOrigin getCrossOrigin() { + return crossOrigin; + } + /** * Specify the type of payload to be generated when the inbound HTTP request content is read by the * {@link HttpMessageConverter}s. By default this value is null which means at runtime any "text" Content-Type will diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java index 826fb1e9bc..69d8186214 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java @@ -27,11 +27,15 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.web.HttpRequestHandler; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.mvc.condition.NameValueExpression; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @@ -98,10 +102,50 @@ public final class IntegrationRequestMappingHandlerMapping extends RequestMappin } RequestMappingInfo mapping = this.getMappingForEndpoint((HttpRequestHandlingEndpointSupport) handler); if (mapping != null) { - this.registerMapping(mapping, handler, HANDLE_REQUEST_METHOD); + registerMapping(mapping, handler, HANDLE_REQUEST_METHOD); } } + @Override + protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { + CrossOrigin crossOrigin = ((HttpRequestHandlingEndpointSupport) handler).getCrossOrigin(); + if (crossOrigin != null) { + CorsConfiguration config = new CorsConfiguration(); + for (String origin : crossOrigin.getOrigin()) { + config.addAllowedOrigin(origin); + } + for (RequestMethod requestMethod : crossOrigin.getMethod()) { + config.addAllowedMethod(requestMethod.name()); + } + for (String header : crossOrigin.getAllowedHeaders()) { + config.addAllowedHeader(header); + } + for (String header : crossOrigin.getExposedHeaders()) { + config.addExposedHeader(header); + } + if (crossOrigin.getAllowCredentials() != null) { + config.setAllowCredentials(crossOrigin.getAllowCredentials()); + } + if (crossOrigin.getMaxAge() != -1) { + config.setMaxAge(crossOrigin.getMaxAge()); + } + if (CollectionUtils.isEmpty(config.getAllowedMethods())) { + for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) { + config.addAllowedMethod(allowedMethod.name()); + } + } + if (CollectionUtils.isEmpty(config.getAllowedHeaders())) { + for (NameValueExpression headerExpression : mappingInfo.getHeadersCondition().getExpressions()) { + if (!headerExpression.isNegated()) { + config.addAllowedHeader(headerExpression.getName()); + } + } + } + return config; + } + return null; + } + /** * Created a {@link RequestMappingInfo} from a * 'Spring Integration HTTP Inbound Endpoint' {@link RequestMapping}. diff --git a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.2.xsd b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.2.xsd index ece1c5f4d7..084c6951d5 100644 --- a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.2.xsd +++ b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-4.2.xsd @@ -32,6 +32,13 @@ + + + + Marks this endpoint as permitting cross origin requests (CORS). + + + @@ -108,6 +115,13 @@ + + + + Marks this endpoint as permitting cross origin requests (CORS). + + + @@ -589,6 +603,79 @@ + + + + Defines configuration for org.springframework.web.cors.CorsConfiguration. + + + + + + List of allowed origins. "*" means that all origins are allowed. These values + are placed in the 'Access-Control-Allow-Origin' header of both the pre-flight + and actual responses. Default value is "*". + + + + + + + Indicates which request headers can be used during the actual request. "*" means + that all headers asked by the client are allowed. This property controls the value of + pre-flight response's 'Access-Control-Allow-Headers' header. + Default value is "*". + + + + + + + List of response headers that the user-agent will allow the client to access. This property + controls the value of actual response's 'Access-Control-Expose-Headers' header. + + + + + + + The HTTP request methods to allow: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE. + Methods specified here overrides 'supported-methods' ones. + + + + + + + + + + Set to "true" if the the browser should include any cookies associated to the domain + of the request being annotated, or "false" if it should not. Empty string "" means undefined. + If true, the pre-flight response will include the header + 'Access-Control-Allow-Credentials=true'. Default value is "true". + + + + + + + + + + Controls the cache duration for pre-flight responses. Setting this to a reasonable + value can reduce the number of pre-flight request/response interaction required by + the browser. This property controls the value of the 'Access-Control-Max-Age' header + in the pre-flight response. Value set to '-1' means undefined. + Default value is 1800 seconds, or 30 minutes. + + + + + + + + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/CrossOriginTests-context.xml b/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/CrossOriginTests-context.xml new file mode 100644 index 0000000000..b0ba72eaad --- /dev/null +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/CrossOriginTests-context.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/CrossOriginTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/CrossOriginTests.java new file mode 100644 index 0000000000..2a4d6d6d6e --- /dev/null +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/CrossOriginTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.http.inbound; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * @author Artem Bilan + * @since 4.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@DirtiesContext +public class CrossOriginTests { + + @Autowired + private IntegrationRequestMappingHandlerMapping handlerMapping; + + private MockHttpServletRequest request; + + @Before + public void setUp() { + this.request = new MockHttpServletRequest(); + this.request.setMethod("GET"); + this.request.addHeader(HttpHeaders.ORIGIN, "http://domain.com/"); + } + + @Test + public void noEndpointWithoutOriginHeader() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/no"); + HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + CorsConfiguration config = getCorsConfiguration(chain, false); + assertNull(config); + } + + @Test + public void noEndpointWithOriginHeader() throws Exception { + this.request.setRequestURI("/no"); + HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + CorsConfiguration config = getCorsConfiguration(chain, false); + assertNull(config); + } + + @Test + public void noEndpointPostWithOriginHeader() throws Exception { + this.request.setMethod("POST"); + this.request.setRequestURI("/no"); + HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + CorsConfiguration config = getCorsConfiguration(chain, false); + assertNull(config); + } + + @Test + public void defaultEndpointWithCrossOrigin() throws Exception { + this.request.setRequestURI("/default"); + HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + CorsConfiguration config = getCorsConfiguration(chain, false); + assertNotNull(config); + assertArrayEquals(new String[]{"GET"}, config.getAllowedMethods().toArray()); + assertArrayEquals(new String[]{"*"}, config.getAllowedOrigins().toArray()); + assertTrue(config.getAllowCredentials()); + assertArrayEquals(new String[]{"*"}, config.getAllowedHeaders().toArray()); + assertNull(config.getExposedHeaders()); + assertEquals(new Long(1800), config.getMaxAge()); + } + + @Test + public void customized() throws Exception { + this.request.setRequestURI("/customized"); + HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + CorsConfiguration config = getCorsConfiguration(chain, false); + assertNotNull(config); + assertArrayEquals(new String[]{"DELETE"}, config.getAllowedMethods().toArray()); + assertArrayEquals(new String[]{"http://site1.com", "http://site2.com"}, config.getAllowedOrigins().toArray()); + assertArrayEquals(new String[]{"header1", "header2"}, config.getAllowedHeaders().toArray()); + assertArrayEquals(new String[]{"header3", "header4"}, config.getExposedHeaders().toArray()); + assertEquals(new Long(123), config.getMaxAge()); + assertEquals(false, config.getAllowCredentials()); + } + + @Test + public void preFlightRequest() throws Exception { + this.request.setMethod("OPTIONS"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.request.setRequestURI("/default"); + HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + CorsConfiguration config = getCorsConfiguration(chain, true); + assertNotNull(config); + assertArrayEquals(new String[]{"GET"}, config.getAllowedMethods().toArray()); + assertArrayEquals(new String[]{"*"}, config.getAllowedOrigins().toArray()); + assertTrue(config.getAllowCredentials()); + assertArrayEquals(new String[]{"*"}, config.getAllowedHeaders().toArray()); + assertNull(config.getExposedHeaders()); + assertEquals(new Long(1800), config.getMaxAge()); + } + + @Test + public void ambiguousHeaderPreFlightRequest() throws Exception { + this.request.setMethod("OPTIONS"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "header1"); + this.request.setRequestURI("/ambiguous-header"); + HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + CorsConfiguration config = getCorsConfiguration(chain, true); + assertNotNull(config); + assertArrayEquals(new String[]{"*"}, config.getAllowedMethods().toArray()); + assertArrayEquals(new String[]{"*"}, config.getAllowedOrigins().toArray()); + assertArrayEquals(new String[]{"*"}, config.getAllowedHeaders().toArray()); + assertTrue(config.getAllowCredentials()); + assertNull(config.getExposedHeaders()); + assertNull(config.getMaxAge()); + } + + @Test + public void ambiguousProducesPreFlightRequest() throws Exception { + this.request.setMethod("OPTIONS"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.request.setRequestURI("/ambiguous-produces"); + HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); + CorsConfiguration config = getCorsConfiguration(chain, true); + assertNotNull(config); + assertArrayEquals(new String[]{"*"}, config.getAllowedMethods().toArray()); + assertArrayEquals(new String[]{"*"}, config.getAllowedOrigins().toArray()); + assertArrayEquals(new String[]{"*"}, config.getAllowedHeaders().toArray()); + assertTrue(config.getAllowCredentials()); + assertNull(config.getExposedHeaders()); + assertNull(config.getMaxAge()); + } + + @Test(expected = HttpRequestMethodNotSupportedException.class) + public void preFlightRequestWithoutRequestMethodHeader() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/default"); + request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com"); + this.handlerMapping.getHandler(request); + } + + private CorsConfiguration getCorsConfiguration(HandlerExecutionChain chain, boolean isPreFlightRequest) { + if (isPreFlightRequest) { + Object handler = chain.getHandler(); + assertTrue(handler.getClass().getSimpleName().equals("PreFlightHandler")); + return TestUtils.getPropertyValue(handler, "config", CorsConfiguration.class); + } + else { + HandlerInterceptor[] interceptors = chain.getInterceptors(); + if (interceptors != null) { + for (HandlerInterceptor interceptor : interceptors) { + if (interceptor.getClass().getSimpleName().equals("CorsInterceptor")) { + return TestUtils.getPropertyValue(interceptor, "config", CorsConfiguration.class); + } + } + } + } + return null; + } + +} diff --git a/src/reference/asciidoc/http.adoc b/src/reference/asciidoc/http.adoc index bcb4a3e84d..c88e4802a4 100644 --- a/src/reference/asciidoc/http.adoc +++ b/src/reference/asciidoc/http.adoc @@ -156,6 +156,8 @@ Of course, this can be an abstract class, or even an interface (such as `java.io [[http-namespace]] === HTTP Namespace Support +==== Introduction + Spring Integration provides an _http_ namespace and the corresponding schema definition. To include it in your configuration, simply provide the following namespace declaration in your application context configuration file: @@ -177,7 +179,7 @@ To include it in your configuration, simply provide the following namespace decl ---- -_Inbound_ +==== Inbound The XML Namespace provides two components for handling HTTP Inbound requests. In order to process requests without returning a dedicated response, use the _inbound-channel-adapter_: @@ -197,7 +199,7 @@ To process requests that do expect a response, use an _inbound-gateway_: reply-channel="responses"/> ---- -_Request Mapping support_ +==== Request Mapping Support NOTE: _Spring Integration 3.0_ is improving the REST support by introducing the http://static.springsource.org/spring-integration/api/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.html[IntegrationRequestMappingHandlerMapping]. The implementation relies on the enhanced REST support provided by Spring Framework 3.1 or higher. @@ -265,9 +267,48 @@ This allows you to get the `Message` into the flow as early as possibly, e.g.: For more information regarding _Handler Mappings_, please see: http://static.springsource.org/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-handlermapping[Handler Mappings]. +[[cors]] +==== Cross-Origin Resource Sharing (CORS) Support +Starting with _version 4.2_ the `` and `` can be configured with +a `` sub-element. +It represents the same options as Spring MVC's `@CrossOrigin` for `@Controller` methods +and allows the configuration of Cross-origin resource sharing (CORS) for Spring Integration HTTP endpoints: -_Response StatusCode_ +* `origin` - List of allowed origins. +`*` means that all origins are allowed. +These values are placed in the `Access-Control-Allow-Origin` header of both the pre-flight +and actual responses. +Default value is `*`. + +* `allowed-headers` - Indicates which request headers can be used during the actual request. +`*` means that all headers asked by the client are allowed. +This property controls the value of the pre-flight response's `Access-Control-Allow-Headers` header. +Default value is `*`. + +* `exposed-headers` - List of response headers that the user-agent will allow the client to access. +This property controls the value of the actual response's `Access-Control-Expose-Headers` header. + +* `method` - The HTTP request methods to allow: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE. +Methods specified here overrides those in `supported-methods`. + +* `allow-credentials` - Set to `true` if the the browser should include any cookies associated to the domain +of the request, or `false` if it should not. +Empty string "" means undefined. +If `true`, the pre-flight response will include the header `Access-Control-Allow-Credentials=true`. +Default value is `true`. + +* `max-age` - Controls the cache duration for pre-flight responses. +Setting this to a reasonable value can reduce the number of pre-flight request/response interactions required by +the browser. +This property controls the value of the `Access-Control-Max-Age` header in the pre-flight response. +A value of `-1` means undefined. +Default value is 1800 seconds, or 30 minutes. + +The CORS Java Configuration is represented by the `org.springframework.integration.http.inbound.CrossOrigin` class, +instances of which can be injected to the `HttpRequestHandlingEndpointSupport` beans. + +==== Response StatusCode Starting with _version 4.1_ the `` can be configured with a `status-code-expression` to override the default `200 OK` status. The expression must return an object which can be converted to an `org.springframework.http.HttpStatus` enum value. @@ -285,7 +326,7 @@ By default, `status-code-expression` is null meaning that the normal '200 OK' re The `` resolves the 'status code' from the `http_statusCode` header of the reply Message. -_URI Template Variables and Expressions_ +==== URI Template Variables and Expressions By Using the _path_ attribute in conjunction with the _payload-expression_ attribute as well as the _header_ sub-element, you have a high degree of flexibility for mapping inbound request data. @@ -334,7 +375,7 @@ Note, all these values (and others) can be accessed within expressions in the do ---- -_Outbound_ +==== Outbound To configure the outbound gateway you can use the namespace support as well. The following code snippet shows the different configuration options for an outbound Http gateway. @@ -417,7 +458,7 @@ Changes in Spring 3.1 can cause some issues with escaped characters, such as '?' For this reason, it is recommended that if you wish to generate the URL entirely at runtime, you use the 'url-expression' attribute. ===== -_Mapping URI Variables_ +==== Mapping URI Variables If your URL contains URI variables, you can map them using the `uri-variable` sub-element. This sub-element is available for the _Http Outbound Gateway_ and the _Http Outbound Channel Adapter_. @@ -475,7 +516,7 @@ NOTE: The `uri-variables-expression` must evaluate to a `Map`. The values of the Map must be instances of `String` or `Expression`. This Map is provided to an `ExpressionEvalMap` for further resolution of URI variable placeholders using those expressions in the context of the outbound `Message`. -_Controlling URI Encoding_ +==== Controlling URI Encoding By default, the URL string is encoded (see http://static.springsource.org/spring/docs/current/javadoc-api/org/springframework/web/util/UriComponentsBuilder.html[UriComponentsBuilder]) to the URI object before sending the request. In some scenarios with a non-standard URI (e.g. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index d90d335f88..e1893daa80 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -116,3 +116,11 @@ for the internal `javax.xml.transform.Transformer` and supports an `Iterator` mo evaluation `org.w3c.dom.NodeList` result. See <> for more information. + +[[x4.2-http-changes]] +==== HTTP changes + +The HTTP Inbound Endpoints (`` and ``) now allow the +configuration of _Cross-Origin Resource Sharing (CORS)_. + +See <> for more information.