Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ private void parseCrossOrigin(Element element, BeanDefinitionBuilder builder) {
if (crossOriginElement != null) {
BeanDefinitionBuilder crossOriginBuilder =
BeanDefinitionBuilder.genericBeanDefinition(CrossOrigin.class);
String[] attributes = {"origin", "allowed-headers", "exposed-headers", "max-age", "method"};
String[] attributes =
{"origin", "origin-patterns", "allowed-headers", "exposed-headers", "max-age", "method"};
for (String crossOriginAttribute : attributes) {
IntegrationNamespaceUtils.setValueIfAttributeDefined(crossOriginBuilder, crossOriginElement,
crossOriginAttribute);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,18 @@ public CrossOriginSpec origin(String... origin) {
return this;
}

/**
* Alternative to {@link #origin} that supports more flexible origin
* patterns.
* @param originPatterns the list of allowed origins.
* @return the spec
* @since 6.5.9
*/
public CrossOriginSpec originPatterns(String... originPatterns) {
this.crossOrigin.setOriginPatterns(originPatterns);
return this;
}

/**
* List of request headers that can be used during the actual request.
* <p>This property controls the value of the pre-flight response's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@
package org.springframework.integration.http.inbound;

import java.util.Arrays;
import java.util.List;

import org.jspecify.annotations.Nullable;

import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestMethod;

/**
* The mapping to permit cross origin requests (CORS) for {@link HttpRequestHandlingEndpointSupport}.
* 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}.
*
Expand All @@ -34,48 +38,83 @@
*/
public class CrossOrigin {

private String[] origin = {"*"};
private String[] origin = {};

private String[] originPatterns = {};

private String[] allowedHeaders = {"*"};
private String[] allowedHeaders = {};

private String[] exposedHeaders = {};

private RequestMethod[] method = {};

private Boolean allowCredentials = true;
private Boolean allowCredentials = false;

private long maxAge = 1800; // NOSONAR magic number
private long maxAge = 1800L;

public void setOrigin(String... origin) {
this.origin = Arrays.copyOf(origin, origin.length);
}

public String[] getOrigin() {
return this.origin; // NOSONAR - expose internals
return this.origin;
}

@Nullable
public List<String> getOriginsList() {
return ObjectUtils.isEmpty(this.origin) ? null : Arrays.asList(this.origin);
}

public void setOriginPatterns(String... originPatterns) {
this.originPatterns = Arrays.copyOf(originPatterns, originPatterns.length);
}

public String[] getOriginPatterns() {
return this.originPatterns;
}

@Nullable
public List<String> getOriginPatternsList() {
return ObjectUtils.isEmpty(this.originPatterns) ? null : Arrays.asList(this.originPatterns);
}

public void setAllowedHeaders(String... allowedHeaders) {
this.allowedHeaders = Arrays.copyOf(allowedHeaders, allowedHeaders.length);
}

public String[] getAllowedHeaders() {
return this.allowedHeaders; // NOSONAR - expose internals
return this.allowedHeaders;
}

@Nullable
public List<String> getAllowedHeadersList() {
return ObjectUtils.isEmpty(this.allowedHeaders) ? null : Arrays.asList(this.allowedHeaders);
}

public void setExposedHeaders(String... exposedHeaders) {
this.exposedHeaders = Arrays.copyOf(exposedHeaders, exposedHeaders.length);
}

public String[] getExposedHeaders() {
return this.exposedHeaders; // NOSONAR - expose internals
return this.exposedHeaders;
}

@Nullable
public List<String> getExposedHeadersList() {
return ObjectUtils.isEmpty(this.exposedHeaders) ? null : Arrays.asList(this.exposedHeaders);
}

public void setMethod(RequestMethod... method) {
this.method = Arrays.copyOf(method, method.length);
}

public RequestMethod[] getMethod() {
return this.method; // NOSONAR - expose internals
return this.method;
}

@Nullable
public List<RequestMethod> getMethodsList() {
return ObjectUtils.isEmpty(this.method) ? null : Arrays.asList(this.method);
}

public void setAllowCredentials(Boolean allowCredentials) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package org.springframework.integration.http.inbound;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -161,28 +160,23 @@ private static CorsConfiguration buildCorsConfiguration(CrossOrigin crossOrigin,
for (RequestMethod requestMethod : crossOrigin.getMethod()) {
config.addAllowedMethod(requestMethod.name());
}
config.setAllowedHeaders(Arrays.asList(crossOrigin.getAllowedHeaders()));
config.setExposedHeaders(Arrays.asList(crossOrigin.getExposedHeaders()));
Boolean allowCredentials = crossOrigin.getAllowCredentials();
config.setAllowCredentials(allowCredentials);
List<String> allowedOrigins = Arrays.asList(crossOrigin.getOrigin());
if (Boolean.TRUE.equals(allowCredentials)
&& CollectionUtils.contains(allowedOrigins.iterator(), CorsConfiguration.ALL)) {
config.setAllowedOriginPatterns(allowedOrigins);
}
else {
config.setAllowedOrigins(allowedOrigins);
}

if (crossOrigin.getMaxAge() != -1) {
config.setMaxAge(crossOrigin.getMaxAge());
List<String> allowedHeadersList = crossOrigin.getAllowedHeadersList();
config.setAllowedHeaders(allowedHeadersList);
config.setExposedHeaders(crossOrigin.getExposedHeadersList());
config.setAllowCredentials(crossOrigin.getAllowCredentials());
config.setAllowedOrigins(crossOrigin.getOriginsList());
config.setAllowedOriginPatterns(crossOrigin.getOriginPatternsList());

long maxAge = crossOrigin.getMaxAge();
if (maxAge != -1) {
config.setMaxAge(maxAge);
}
if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
config.addAllowedMethod(allowedMethod.name());
}
}
if (CollectionUtils.isEmpty(config.getAllowedHeaders())) {
if (CollectionUtils.isEmpty(allowedHeadersList)) {
for (NameValueExpression<String> headerExpression :
mappingInfo.getHeadersCondition().getExpressions()) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
<xsd:element name="cross-origin" type="crossOriginType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Marks this endpoint as permitting cross origin requests (CORS).
Marks this endpoint as permitting cross-origin requests (CORS).
</xsd:documentation>
</xsd:annotation>
</xsd:element>
Expand Down Expand Up @@ -716,16 +716,24 @@
Defines configuration for org.springframework.web.cors.CorsConfiguration.
</xsd:documentation>
</xsd:annotation>
<xsd:attribute name="origin" type="xsd:string" default="*">
<xsd:attribute name="origin" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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 "*".
and actual responses.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="allowed-headers" type="xsd:string" default="*">
<xsd:attribute name="origin-patterns" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Alternative list to `origin` that supports more flexible
origins patterns with "*" anywhere in the host name in addition to port lists.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="allowed-headers" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Indicates which request headers can be used during the actual request. "*" means
Expand All @@ -747,20 +755,20 @@
<xsd:annotation>
<xsd:documentation>
The HTTP request methods to allow: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
Methods specified here overrides 'supported-methods' ones.
Methods specified here override 'supported-methods' ones.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="httpMethodEnumeration xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="allow-credentials" default="true">
<xsd:attribute name="allow-credentials">
<xsd:annotation>
<xsd:documentation>
Set to "true" if the browser should include any cookies associated to the domain
Set to "true" if the browser should include any cookies associated with 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".
'Access-Control-Allow-Credentials=true'.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
Expand All @@ -771,7 +779,7 @@
<xsd:annotation>
<xsd:documentation>
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
value can reduce the amount 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ public void defaultEndpointWithCrossOrigin() throws Exception {
HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request);
CorsConfiguration config = getCorsConfiguration(chain, false);
assertThat(config).isNotNull();
assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"GET"});
assertThat(config.getAllowedOrigins()).isNull();
assertThat(config.getAllowedOriginPatterns().toArray()).isEqualTo(new String[] {"*"});
assertThat(config.getAllowCredentials()).isTrue();
assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[] {"*"});
assertThat(config.getExposedHeaders()).isEmpty();
assertThat(config.getAllowedMethods()).containsOnly("GET");
assertThat(config.getAllowedOrigins()).containsOnly("*");
assertThat(config.getAllowedOriginPatterns()).isNull();
assertThat(config.getAllowCredentials()).isFalse();
assertThat(config.getAllowedHeaders()).containsOnly("*");
assertThat(config.getExposedHeaders()).isNull();
assertThat(config.getMaxAge()).isEqualTo(1800L);
}

Expand All @@ -117,12 +117,12 @@ public void preFlightRequest() throws Exception {
HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request);
CorsConfiguration config = getCorsConfiguration(chain, true);
assertThat(config).isNotNull();
assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[] {"GET"});
assertThat(config.getAllowedOrigins()).isNull();
assertThat(config.getAllowedOriginPatterns().toArray()).isEqualTo(new String[] {"*"});
assertThat(config.getAllowCredentials()).isTrue();
assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[] {"*"});
assertThat(config.getExposedHeaders()).isEmpty();
assertThat(config.getAllowedMethods()).containsOnly("GET");
assertThat(config.getAllowedOrigins()).containsOnly("*");
assertThat(config.getAllowedOriginPatterns()).isNull();
assertThat(config.getAllowCredentials()).isFalse();
assertThat(config.getAllowedHeaders()).containsOnly("*");
assertThat(config.getExposedHeaders()).isNull();
assertThat(config.getMaxAge()).isEqualTo(1800L);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package org.springframework.integration.webflux.inbound;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

Expand Down Expand Up @@ -166,28 +165,23 @@ private static CorsConfiguration buildCorsConfiguration(CrossOrigin crossOrigin,
for (RequestMethod requestMethod : crossOrigin.getMethod()) {
config.addAllowedMethod(requestMethod.name());
}
config.setAllowedHeaders(Arrays.asList(crossOrigin.getAllowedHeaders()));
config.setExposedHeaders(Arrays.asList(crossOrigin.getExposedHeaders()));
Boolean allowCredentials = crossOrigin.getAllowCredentials();
config.setAllowCredentials(allowCredentials);
List<String> allowedOrigins = Arrays.asList(crossOrigin.getOrigin());
if (Boolean.TRUE.equals(allowCredentials)
&& CollectionUtils.contains(allowedOrigins.iterator(), CorsConfiguration.ALL)) {
config.setAllowedOriginPatterns(allowedOrigins);
}
else {
config.setAllowedOrigins(allowedOrigins);
}

if (crossOrigin.getMaxAge() != -1) {
config.setMaxAge(crossOrigin.getMaxAge());
List<String> allowedHeadersList = crossOrigin.getAllowedHeadersList();
config.setAllowedHeaders(allowedHeadersList);
config.setExposedHeaders(crossOrigin.getExposedHeadersList());
config.setAllowCredentials(crossOrigin.getAllowCredentials());
config.setAllowedOrigins(crossOrigin.getOriginsList());
config.setAllowedOriginPatterns(crossOrigin.getOriginPatternsList());

long maxAge = crossOrigin.getMaxAge();
if (maxAge != -1) {
config.setMaxAge(maxAge);
}
if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
config.addAllowedMethod(allowedMethod.name());
}
}
if (CollectionUtils.isEmpty(config.getAllowedHeaders())) {
if (CollectionUtils.isEmpty(allowedHeadersList)) {
for (NameValueExpression<String> headerExpression :
mappingInfo.getHeadersCondition().getExpressions()) {

Expand Down
18 changes: 10 additions & 8 deletions src/reference/antora/modules/ROOT/pages/http/namespace.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -122,24 +122,27 @@ For this reason, configuring the same path for both Spring Integration and Sprin
== Cross-origin Resource Sharing (CORS) Support

Starting with version 4.2, you can configure the `<http:inbound-channel-adapter>` and `<http:inbound-gateway>` with a `<cross-origin>` element.
It represents the same options as Spring MVC's `@CrossOrigin` for `@Controller` annotations and allows the configuration of cross-origin resource sharing (CORS) for Spring Integration HTTP endpoints:
It represents the same options as Spring MVC `@CrossOrigin` for `@Controller` annotations and allows the configuration of cross-origin resource sharing (CORS) for Spring Integration HTTP endpoints:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add this to the What's new doc as well, since this is a breaking change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we do not.
This is going to be back-ported down to 6.5.x.
And this is essentially a fix for already not working code.
To make it working you have to change those defaults.
Therefore everyone who uses this feature already covered, otherwise it would fail for them with those defaults rejected by clients.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Just had to ask the question.


* `origin`: List of allowed origins.
The `pass:[*]` 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.
The default value is `pass:[*]`.
The default value is empty.
* `origin-patterns`: List of allowed origin patterns.
Alternative list to `origin` that supports more flexible origins patterns with `pass:[*]` anywhere in the host name in addition to port lists.
The default value is empty.
* `allowed-headers`: Indicates which request headers can be used during the actual request.
The `pass:[*]` means that all headers requested by the client are allowed.
This property controls the value of the pre-flight response's `Access-Control-Allow-Headers` header.
The default value is `pass:[*]`.
The default value is empty.
* `exposed-headers`: List of response headers that the user-agent lets the client 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 browser should include any cookies associated to the domain of the request or `false` if it should not.
An empty string ("") means undefined.
Methods specified here override those in `supported-methods`.
* `allow-credentials`: Set to `true` if the browser should include any cookies associated with the domain of the request or `false` if it should not.
An empty string means undefined.
If `true`, the pre-flight response includes the `Access-Control-Allow-Credentials=true` header.
The default value is `true`.
The default value is empty.
* `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.
Expand Down Expand Up @@ -470,4 +473,3 @@ If you wish to partially encode some part of the URL, use an `expression` within
With Java DSL this option can be controlled by the `BaseHttpMessageHandlerSpec.encodingMode()` option.
The same configuration applies for similar outbound components in the xref:webflux.adoc[WebFlux module] and xref:ws.adoc[Web Services module].
For much sophisticated scenarios it is recommended to configure an `UriTemplateHandler` on the externally provided `RestTemplate`; or in case of WebFlux - `WebClient` with it `UriBuilderFactory`.