Add Reactive CORS support

This is a port of Spring MVC CORS support for Spring Web Reactive:
 - CORS classes keep the same name but are in the
   web.cors.reactive package
 - CorsConfiguration is reused because not tied to Servlet API
 - CORS HandlerMapping integration is done at
   AbstractHandlerMapping level
 - AbstractUrlHandlerMapping and AbstractHandlerMethodMapping
   have been slightly modified to call
   AbstractHandlerMapping#processCorsRequest()
 - Both global CORS configuration + @CrossOrigin support have
   been implemented

Issue: SPR-14545
This commit is contained in:
Sebastien Deleuze
2016-10-04 19:19:42 +02:00
committed by Rossen Stoyanchev
parent 0cc330e8fc
commit e31a2f778b
25 changed files with 2179 additions and 26 deletions

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2002-2016 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.web.cors.reactive;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.server.ServerWebExchange;
/**
* Interface to be implemented by classes (usually HTTP request handlers) that
* provides a {@link CorsConfiguration} instance based on the provided reactive request.
*
* @author Sebastien Deleuze
* @since 5.0
*/
public interface CorsConfigurationSource {
/**
* Return a {@link CorsConfiguration} based on the incoming request.
* @return the associated {@link CorsConfiguration}, or {@code null} if none
*/
CorsConfiguration getCorsConfiguration(ServerWebExchange exchange);
}

View File

@@ -0,0 +1,48 @@
/*
* 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.
* 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.web.cors.reactive;
import reactor.core.publisher.Mono;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.server.ServerWebExchange;
/**
* A strategy that takes a reactive request and a {@link CorsConfiguration} and updates
* the response.
*
* <p>This component is not concerned with how a {@code CorsConfiguration} is
* selected but rather takes follow-up actions such as applying CORS validation
* checks and either rejecting the response or adding CORS headers to the
* response.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @since 5.0
* @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>
*/
public interface CorsProcessor {
/**
* Process a request given a {@code CorsConfiguration}.
* @param configuration the applicable CORS configuration (possibly {@code null})
* @param exchange the current HTTP request / response
* @return a {@link Mono} emitting {@code false} if the request is rejected, {@code true} otherwise
*/
boolean processRequest(CorsConfiguration configuration, ServerWebExchange exchange);
}

View File

@@ -0,0 +1,86 @@
/*
* 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.
* 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.web.cors.reactive;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
;
/**
* Utility class for CORS reactive request handling based on the
* <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a>.
*
* @author Sebastien Deleuze
* @since 5.0
*/
public abstract class CorsUtils {
/**
* Returns {@code true} if the request is a valid CORS one.
*/
public static boolean isCorsRequest(ServerHttpRequest request) {
return (request.getHeaders().get(HttpHeaders.ORIGIN) != null);
}
/**
* Returns {@code true} if the request is a valid CORS pre-flight one.
*/
public static boolean isPreFlightRequest(ServerHttpRequest request) {
return (isCorsRequest(request) && HttpMethod.OPTIONS == request.getMethod() &&
request.getHeaders().get(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
}
/**
* Check if the request is a same-origin one, based on {@code Origin}, {@code Host},
* {@code Forwarded} and {@code X-Forwarded-Host} headers.
* @return {@code true} if the request is a same-origin one, {@code false} in case
* of cross-origin request.
*/
public static boolean isSameOrigin(ServerHttpRequest request) {
String origin = request.getHeaders().getOrigin();
if (origin == null) {
return true;
}
UriComponentsBuilder urlBuilder = UriComponentsBuilder.fromHttpRequest(request);
UriComponents actualUrl = urlBuilder.build();
String actualHost = actualUrl.getHost();
int actualPort = getPort(actualUrl);
Assert.notNull(actualHost, "Actual request host must not be null");
Assert.isTrue(actualPort != -1, "Actual request port must not be undefined");
UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
return (actualHost.equals(originUrl.getHost()) && actualPort == getPort(originUrl));
}
private static int getPort(UriComponents uri) {
int port = uri.getPort();
if (port == -1) {
if ("http".equals(uri.getScheme()) || "ws".equals(uri.getScheme())) {
port = 80;
}
else if ("https".equals(uri.getScheme()) || "wss".equals(uri.getScheme())) {
port = 443;
}
}
return port;
}
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2002-2016 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.web.cors.reactive;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.WebUtils;
/**
* The default implementation of {@link CorsProcessor},
* as defined by the <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a>.
*
* <p>Note that when input {@link CorsConfiguration} is {@code null}, this
* implementation does not reject simple or actual requests outright but simply
* avoid adding CORS headers to the response. CORS processing is also skipped
* if the response already contains CORS headers, or if the request is detected
* as a same-origin one.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @since 5.0
*/
public class DefaultCorsProcessor implements CorsProcessor {
private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class);
@Override
@SuppressWarnings("resource")
public boolean processRequest(CorsConfiguration config, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
if (responseHasCors(response)) {
logger.debug("Skip CORS processing: response already contains \"Access-Control-Allow-Origin\" header");
return true;
}
if (CorsUtils.isSameOrigin(request)) {
logger.debug("Skip CORS processing: request is from same origin");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(response);
return false;
}
else {
return true;
}
}
return handleInternal(exchange, config, preFlightRequest);
}
private boolean responseHasCors(ServerHttpResponse response) {
return (response.getHeaders().getAccessControlAllowOrigin() != null);
}
/**
* Invoked when one of the CORS checks failed.
*/
protected void rejectRequest(ServerHttpResponse response) {
response.setStatusCode(HttpStatus.FORBIDDEN);
logger.debug("Invalid CORS request");
}
/**
* Handle the given request.
*/
protected boolean handleInternal(ServerWebExchange exchange,
CorsConfiguration config, boolean preFlightRequest) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
List<String> allowHeaders = checkHeaders(config, requestHeaders);
if (allowOrigin == null || allowMethods == null || (preFlightRequest && allowHeaders == null)) {
rejectRequest(response);
return false;
}
HttpHeaders responseHeaders = response.getHeaders();
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
responseHeaders.add(HttpHeaders.VARY, HttpHeaders.ORIGIN);
if (preFlightRequest) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
return true;
}
/**
* Check the origin and determine the origin for the response. The default
* implementation simply delegates to
* {@link CorsConfiguration#checkOrigin(String)}.
*/
protected String checkOrigin(CorsConfiguration config, String requestOrigin) {
return config.checkOrigin(requestOrigin);
}
/**
* Check the HTTP method and determine the methods for the response of a
* pre-flight request. The default implementation simply delegates to
* {@link CorsConfiguration#checkOrigin(String)}.
*/
protected List<HttpMethod> checkMethods(CorsConfiguration config, HttpMethod requestMethod) {
return config.checkHttpMethod(requestMethod);
}
private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight) {
return (isPreFlight ? request.getHeaders().getAccessControlRequestMethod() : request.getMethod());
}
/**
* Check the headers and determine the headers for the response of a
* pre-flight request. The default implementation simply delegates to
* {@link CorsConfiguration#checkOrigin(String)}.
*/
protected List<String> checkHeaders(CorsConfiguration config, List<String> requestHeaders) {
return config.checkHeaders(requestHeaders);
}
private List<String> getHeadersToUse(ServerHttpRequest request, boolean isPreFlight) {
HttpHeaders headers = request.getHeaders();
return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<>(headers.keySet()));
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2002-2016 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.web.cors.reactive;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.HttpRequestPathHelper;
/**
* Provide a per reactive request {@link CorsConfiguration} instance based on a
* collection of {@link CorsConfiguration} mapped on path patterns.
*
* <p>Exact path mapping URIs (such as {@code "/admin"}) are supported
* as well as Ant-style path patterns (such as {@code "/admin/**"}).
*
* @author Sebastien Deleuze
* @since 5.0
*/
public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource {
private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
private PathMatcher pathMatcher = new AntPathMatcher();
private HttpRequestPathHelper pathHelper = new HttpRequestPathHelper();
/**
* Set the PathMatcher implementation to use for matching URL paths
* against registered URL patterns. Default is AntPathMatcher.
* @see AntPathMatcher
*/
public void setPathMatcher(PathMatcher pathMatcher) {
Assert.notNull(pathMatcher, "PathMatcher must not be null");
this.pathMatcher = pathMatcher;
}
/**
* Set if context path and request URI should be URL-decoded. Both are returned
* <i>undecoded</i> by the Servlet API, in contrast to the servlet path.
* <p>Uses either the request encoding or the default encoding according
* to the Servlet spec (ISO-8859-1).
* @see HttpRequestPathHelper#setUrlDecode
*/
public void setUrlDecode(boolean urlDecode) {
this.pathHelper.setUrlDecode(urlDecode);
}
/**
* Set the UrlPathHelper to use for resolution of lookup paths.
* <p>Use this to override the default UrlPathHelper with a custom subclass.
*/
public void setHttpRequestPathHelper(HttpRequestPathHelper pathHelper) {
Assert.notNull(pathHelper, "HttpRequestPathHelper must not be null");
this.pathHelper = pathHelper;
}
/**
* Set CORS configuration based on URL patterns.
*/
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
this.corsConfigurations.clear();
if (corsConfigurations != null) {
this.corsConfigurations.putAll(corsConfigurations);
}
}
/**
* Get the CORS configuration.
*/
public Map<String, CorsConfiguration> getCorsConfigurations() {
return Collections.unmodifiableMap(this.corsConfigurations);
}
/**
* Register a {@link CorsConfiguration} for the specified path pattern.
*/
public void registerCorsConfiguration(String path, CorsConfiguration config) {
this.corsConfigurations.put(path, config);
}
@Override
public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) {
String lookupPath = this.pathHelper.getLookupPathForRequest(exchange);
for (Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
if (this.pathMatcher.match(entry.getKey(), lookupPath)) {
return entry.getValue();
}
}
return null;
}
}

View File

@@ -710,8 +710,8 @@ public class UriComponentsBuilder implements Cloneable {
}
}
if ((this.scheme.equals("http") && "80".equals(this.port)) ||
(this.scheme.equals("https") && "443".equals(this.port))) {
if ((this.scheme != null) && ((this.scheme.equals("http") && "80".equals(this.port)) ||
(this.scheme.equals("https") && "443".equals(this.port)))) {
this.port = null;
}