diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index b094c980fcbd7bff1baf5eab4476a250016097eb..d4fabd101f6ba0e51a74e54e2adcb3e80df62f83 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -33,21 +33,28 @@ import java.util.List; public class CorsConfiguration { private List allowedOrigins; + private List allowedMethods; + private List allowedHeaders; + private List exposedHeaders; + private Boolean allowCredentials; + private Long maxAge; + public CorsConfiguration() { } + public CorsConfiguration(CorsConfiguration config) { if (config.allowedOrigins != null) { this.allowedOrigins = new ArrayList(config.allowedOrigins); } if (config.allowCredentials != null) { - this.allowCredentials = new Boolean(config.allowCredentials); + this.allowCredentials = config.allowCredentials; } if (config.exposedHeaders != null) { this.exposedHeaders = new ArrayList(config.exposedHeaders); @@ -59,7 +66,7 @@ public class CorsConfiguration { this.allowedHeaders = new ArrayList(config.allowedHeaders); } if (config.maxAge != null) { - this.maxAge = new Long(config.maxAge); + this.maxAge = config.maxAge; } } @@ -98,7 +105,7 @@ public class CorsConfiguration { } /** - * Set allowed origins that will define Access-Control-Allow-Origin response + * Set allowed allowedOrigins that will define Access-Control-Allow-Origin response * header values (mandatory). For example "http://domain1.com", "http://domain2.com" ... * "*" means that all domains are allowed. */ diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/CorsProcessor.java index b37b1bd3c95e469ea67a15f9fa1ac8ecd13f65f9..790e9259c15fddead8ea31cd29ffa91e91ceff32 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsProcessor.java @@ -21,7 +21,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** - * Interface to be implemented by classes that process CORS preflight and actual requests. + * Contract for handling CORS preflight requests and intercepting CORS simple + * and actual requests. * * @author Sebastien Deleuze * @since 4.2 @@ -30,21 +31,23 @@ import javax.servlet.http.HttpServletResponse; public interface CorsProcessor { /** - * Process a pre-flight CORS request based on the provided {@link CorsConfiguration}. - * If the request is not a valid CORS pre-flight request or if it does not comply with - * the configuration, it should be rejected. - * If the request is valid and comply with the configuration, this method adds the related - * CORS headers to the response. + * Process a preflight CORS request given a {@link CorsConfiguration}. + * If the request is not a valid CORS pre-flight request or if it does not + * comply with the configuration it should be rejected. + * If the request is valid and complies with the configuration, CORS headers + * should be added to the response. */ - boolean processPreFlightRequest(CorsConfiguration conf, HttpServletRequest request, HttpServletResponse response) throws IOException; + boolean processPreFlightRequest(CorsConfiguration conf, HttpServletRequest request, + HttpServletResponse response) throws IOException; /** - * Process a simple or actual CORS request based on the provided {@link CorsConfiguration}. - * If the request is not a valid CORS simple or actual request or if it does not comply - * with the configuration, it should be rejected. + * Process a simple or actual CORS request given a {@link CorsConfiguration}. + * If the request is not a valid CORS simple or actual request or if it does + * not comply with the configuration, it should be rejected. * If the request is valid and comply with the configuration, this method adds the related * CORS headers to the response. */ - boolean processActualRequest(CorsConfiguration conf, HttpServletRequest request, HttpServletResponse response) throws IOException; + boolean processActualRequest(CorsConfiguration conf, HttpServletRequest request, + HttpServletResponse response) throws IOException; } diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java b/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java index 01efb6d3614c28715529be82e0993a278f33a29b..19afc26ffda9e719d636f21b375d176c24bf00ec 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java @@ -75,6 +75,7 @@ public class CorsUtils { */ public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + /** * Returns {@code true} if the request is a valid CORS one. */ @@ -92,19 +93,4 @@ public class CorsUtils { return request.getMethod().equals(HttpMethod.OPTIONS.name()); } - /** - * Returns {@code true} if the response already contains CORS headers. - */ - public static boolean isCorsResponse(HttpServletResponse response) { - boolean hasCorsResponseHeaders = false; - try { - // Perhaps a CORS Filter has already added this? - hasCorsResponseHeaders = !CollectionUtils.isEmpty(response.getHeaders(CorsUtils.ACCESS_CONTROL_ALLOW_ORIGIN)); - } - catch (NullPointerException npe) { - // See SPR-11919 and https://issues.jboss.org/browse/WFLY-3474 - } - return hasCorsResponseHeaders; - } - } diff --git a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java index c0481a368cc3401d312cfe86a636eb89ccdc9859..96dacf2a473f64b3d1536395950746b043588fb2 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java @@ -19,6 +19,7 @@ package org.springframework.web.cors; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -29,6 +30,8 @@ import org.apache.commons.logging.LogFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -42,16 +45,17 @@ public class DefaultCorsProcessor implements CorsProcessor { protected final Log logger = LogFactory.getLog(getClass()); + @Override - public boolean processPreFlightRequest(CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException { - if (!CorsUtils.isPreFlightRequest(request)) { - rejectCorsRequest(response); - return false; - } + public boolean processPreFlightRequest(CorsConfiguration config, HttpServletRequest request, + HttpServletResponse response) throws IOException { + + Assert.isTrue(CorsUtils.isPreFlightRequest(request)); + if (check(request, response, config)) { - setOriginHeader(request, response, config.getAllowedOrigins(), config.isAllowCredentials()); - setAllowCredentialsHeader(response, config.isAllowCredentials()); - setAllowMethodsHeader(request, response, config.getAllowedMethods()); + setAllowOrigin(request, response, config.getAllowedOrigins(), config.isAllowCredentials()); + setAllowCredentials(response, config.isAllowCredentials()); + setAllowMethods(request, response, config.getAllowedMethods()); setAllowHeadersHeader(request, response, config.getAllowedHeaders()); setMaxAgeHeader(response, config.getMaxAge()); } @@ -59,59 +63,66 @@ public class DefaultCorsProcessor implements CorsProcessor { } @Override - public boolean processActualRequest(CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException { - if (CorsUtils.isPreFlightRequest(request) || !CorsUtils.isCorsRequest(request)) { - rejectCorsRequest(response); - return false; - } + public boolean processActualRequest(CorsConfiguration config, HttpServletRequest request, + HttpServletResponse response) throws IOException { + + Assert.isTrue(CorsUtils.isCorsRequest(request) && !CorsUtils.isPreFlightRequest(request)); + if (check(request, response, config)) { - setOriginHeader(request, response, config.getAllowedOrigins(), config.isAllowCredentials()); - setAllowCredentialsHeader(response, config.isAllowCredentials()); + setAllowOrigin(request, response, config.getAllowedOrigins(), config.isAllowCredentials()); + setAllowCredentials(response, config.isAllowCredentials()); setExposeHeadersHeader(response, config.getExposedHeaders()); } return true; } - private void rejectCorsRequest(HttpServletResponse response) throws IOException { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid CORS request"); - } + private boolean check(HttpServletRequest request, HttpServletResponse response, + CorsConfiguration config) throws IOException { - private boolean check(HttpServletRequest request, HttpServletResponse response, CorsConfiguration config) throws IOException { - if (CorsUtils.isCorsResponse(response)) { + if (hasAllowOriginHeader(response)) { logger.debug("Skip adding CORS headers, response already contains \"Access-Control-Allow-Origin\""); return false; } - if (!(checkOrigin(request, config.getAllowedOrigins()) && - checkRequestMethod(request, config.getAllowedMethods()) && - checkRequestHeaders(request, config.getAllowedHeaders()))) { - rejectCorsRequest(response); + if (!checkOrigin(request, config.getAllowedOrigins()) || !checkMethod(request, config.getAllowedMethods()) || + !checkHeaders(request, config.getAllowedHeaders())) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid CORS request"); return false; } return true; } + private boolean hasAllowOriginHeader(HttpServletResponse response) { + boolean hasCorsResponseHeaders = false; + try { + // Perhaps a CORS Filter has already added this? + Collection headers = response.getHeaders(CorsUtils.ACCESS_CONTROL_ALLOW_ORIGIN); + hasCorsResponseHeaders = !CollectionUtils.isEmpty(headers); + } + catch (NullPointerException npe) { + // See SPR-11919 and https://issues.jboss.org/browse/WFLY-3474 + } + return hasCorsResponseHeaders; + } + private boolean checkOrigin(HttpServletRequest request, List allowedOrigins) { - String origin = request.getHeader(HttpHeaders.ORIGIN); - if ((origin == null) || (allowedOrigins == null)) { + String originHeader = request.getHeader(HttpHeaders.ORIGIN); + if (originHeader == null || allowedOrigins == null) { return false; } if (allowedOrigins.contains("*")) { return true; } for (String allowedOrigin : allowedOrigins) { - if (origin.equalsIgnoreCase(allowedOrigin)) { + if (originHeader.equalsIgnoreCase(allowedOrigin)) { return true; } } return false; } - private boolean checkRequestMethod(HttpServletRequest request, List allowedMethods) { + private boolean checkMethod(HttpServletRequest request, List allowedMethods) { String requestMethod = CorsUtils.isPreFlightRequest(request) ? request.getHeader(CorsUtils.ACCESS_CONTROL_REQUEST_METHOD) : request.getMethod(); - if (requestMethod == null) { - return false; - } if (allowedMethods == null) { allowedMethods = Arrays.asList(HttpMethod.GET.name()); } @@ -119,18 +130,18 @@ public class DefaultCorsProcessor implements CorsProcessor { return true; } for (String allowedMethod : allowedMethods) { - if (requestMethod.equalsIgnoreCase(allowedMethod)) { + if (allowedMethod.equalsIgnoreCase(requestMethod)) { return true; } } return false; } - private boolean checkRequestHeaders(HttpServletRequest request, List allowedHeaders) { + private boolean checkHeaders(HttpServletRequest request, List allowedHeaders) { + String headerValue = request.getHeader(CorsUtils.ACCESS_CONTROL_REQUEST_HEADERS); String[] requestHeaders = CorsUtils.isPreFlightRequest(request) ? - StringUtils.commaDelimitedListToStringArray(request.getHeader(CorsUtils.ACCESS_CONTROL_REQUEST_HEADERS)) : - Collections.list(request.getHeaderNames()).toArray(new String [0]); - + StringUtils.commaDelimitedListToStringArray(headerValue) : + Collections.list(request.getHeaderNames()).toArray(new String[0]); if ((allowedHeaders != null) && allowedHeaders.contains("*")) { return true; } @@ -154,7 +165,9 @@ public class DefaultCorsProcessor implements CorsProcessor { return true; } - private void setOriginHeader(HttpServletRequest request, HttpServletResponse response, List allowedOrigins, Boolean allowCredentials) { + private void setAllowOrigin(HttpServletRequest request, HttpServletResponse response, + List allowedOrigins, Boolean allowCredentials) { + String origin = request.getHeader(HttpHeaders.ORIGIN); if (allowedOrigins.contains("*") && (allowCredentials == null || !allowCredentials)) { response.addHeader(CorsUtils.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); @@ -164,27 +177,26 @@ public class DefaultCorsProcessor implements CorsProcessor { response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN); } - private void setAllowCredentialsHeader(HttpServletResponse response, Boolean allowCredentials) { - if ((allowCredentials != null) && allowCredentials) { - response.addHeader(CorsUtils.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - } - } + private void setAllowMethods(HttpServletRequest request, HttpServletResponse response, + List allowedMethods) { - private void setAllowMethodsHeader(HttpServletRequest request, HttpServletResponse response, List allowedMethods) { if (allowedMethods == null) { allowedMethods = Arrays.asList(HttpMethod.GET.name()); } if (allowedMethods.contains("*")) { - response.addHeader(CorsUtils.ACCESS_CONTROL_ALLOW_METHODS, request.getHeader(CorsUtils.ACCESS_CONTROL_REQUEST_METHOD)); + String headerValue = request.getHeader(CorsUtils.ACCESS_CONTROL_REQUEST_METHOD); + response.addHeader(CorsUtils.ACCESS_CONTROL_ALLOW_METHODS, headerValue); } else { - response.addHeader(CorsUtils.ACCESS_CONTROL_ALLOW_METHODS, StringUtils.collectionToCommaDelimitedString(allowedMethods)); + String headerValue = StringUtils.collectionToCommaDelimitedString(allowedMethods); + response.addHeader(CorsUtils.ACCESS_CONTROL_ALLOW_METHODS, headerValue); } } private void setAllowHeadersHeader(HttpServletRequest request, HttpServletResponse response, List allowedHeaders) { if ((allowedHeaders != null) && !allowedHeaders.isEmpty()) { - String[] requestHeaders = StringUtils.commaDelimitedListToStringArray(request.getHeader(CorsUtils.ACCESS_CONTROL_REQUEST_HEADERS)); + String headerValue = request.getHeader(CorsUtils.ACCESS_CONTROL_REQUEST_HEADERS); + String[] requestHeaders = StringUtils.commaDelimitedListToStringArray(headerValue); boolean matchAll = allowedHeaders.contains("*"); List matchingHeaders = new ArrayList(); for (String requestHeader : requestHeaders) { @@ -197,14 +209,22 @@ public class DefaultCorsProcessor implements CorsProcessor { } } if (!matchingHeaders.isEmpty()) { - response.addHeader(CorsUtils.ACCESS_CONTROL_ALLOW_HEADERS, StringUtils.collectionToCommaDelimitedString(matchingHeaders)); + response.addHeader(CorsUtils.ACCESS_CONTROL_ALLOW_HEADERS, + StringUtils.collectionToCommaDelimitedString(matchingHeaders)); } } } private void setExposeHeadersHeader(HttpServletResponse response, List exposedHeaders) { if ((exposedHeaders != null) && !exposedHeaders.isEmpty()) { - response.addHeader(CorsUtils.ACCESS_CONTROL_EXPOSE_HEADERS, StringUtils.collectionToCommaDelimitedString(exposedHeaders)); + response.addHeader(CorsUtils.ACCESS_CONTROL_EXPOSE_HEADERS, + StringUtils.collectionToCommaDelimitedString(exposedHeaders)); + } + } + + private void setAllowCredentials(HttpServletResponse response, Boolean allowCredentials) { + if ((allowCredentials != null) && allowCredentials) { + response.addHeader(CorsUtils.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); } }