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:
committed by
Rossen Stoyanchev
parent
0cc330e8fc
commit
e31a2f778b
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user