All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.cors.CorsConfiguration Maven / Gradle / Ivy

There is a newer version: 6.1.6
Show newest version
/*
 * Copyright 2002-2022 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
 *
 *      https://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;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * A container for CORS configuration along with methods to check against the
 * actual origin, HTTP methods, and headers of a given request.
 *
 * 

By default a newly created {@code CorsConfiguration} does not permit any * cross-origin requests and must be configured explicitly to indicate what * should be allowed. Use {@link #applyPermitDefaultValues()} to flip the * initialization model to start with open defaults that permit all cross-origin * requests for GET, HEAD, and POST requests. * * @author Sebastien Deleuze * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sam Brannen * @author Ruslan Akhundov * @since 4.2 * @see CORS spec */ public class CorsConfiguration { /** Wildcard representing all origins, methods, or headers. */ public static final String ALL = "*"; private static final List ALL_LIST = Collections.singletonList(ALL); private static final OriginPattern ALL_PATTERN = new OriginPattern("*"); private static final List ALL_PATTERN_LIST = Collections.singletonList(ALL_PATTERN); private static final List DEFAULT_PERMIT_ALL = Collections.singletonList(ALL); private static final List DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.HEAD); private static final List DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); @Nullable private List allowedOrigins; @Nullable private List allowedOriginPatterns; @Nullable private List allowedMethods; @Nullable private List resolvedMethods = DEFAULT_METHODS; @Nullable private List allowedHeaders; @Nullable private List exposedHeaders; @Nullable private Boolean allowCredentials; @Nullable private Long maxAge; /** * Construct a new {@code CorsConfiguration} instance with no cross-origin * requests allowed for any origin by default. * @see #applyPermitDefaultValues() */ public CorsConfiguration() { } /** * Construct a new {@code CorsConfiguration} instance by copying all * values from the supplied {@code CorsConfiguration}. */ public CorsConfiguration(CorsConfiguration other) { this.allowedOrigins = other.allowedOrigins; this.allowedOriginPatterns = other.allowedOriginPatterns; this.allowedMethods = other.allowedMethods; this.resolvedMethods = other.resolvedMethods; this.allowedHeaders = other.allowedHeaders; this.exposedHeaders = other.exposedHeaders; this.allowCredentials = other.allowCredentials; this.maxAge = other.maxAge; } /** * A list of origins for which cross-origin requests are allowed where each * value may be one of the following: *

    *
  • a specific domain, e.g. {@code "https://domain1.com"} *
  • comma-delimited list of specific domains, e.g. * {@code "https://a1.com,https://a2.com"}; this is convenient when a value * is resolved through a property placeholder, e.g. {@code "${origin}"}; * note that such placeholders must be resolved externally. *
  • the CORS defined special value {@code "*"} for all origins *
*

For matched pre-flight and actual requests the * {@code Access-Control-Allow-Origin} response header is set either to the * matched domain value or to {@code "*"}. Keep in mind however that the * CORS spec does not allow {@code "*"} when {@link #setAllowCredentials * allowCredentials} is set to {@code true} and as of 5.3 that combination * is rejected in favor of using {@link #setAllowedOriginPatterns * allowedOriginPatterns} instead. *

By default this is not set which means that no origins are allowed. * However, an instance of this class is often initialized further, e.g. for * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. */ public void setAllowedOrigins(@Nullable List origins) { if (origins == null) { this.allowedOrigins = null; } else { this.allowedOrigins = new ArrayList<>(origins.size()); for (String origin : origins) { addAllowedOrigin(origin); } } } private String trimTrailingSlash(String origin) { return (origin.endsWith("/") ? origin.substring(0, origin.length() - 1) : origin); } /** * Return the configured origins to allow, or {@code null} if none. */ @Nullable public List getAllowedOrigins() { return this.allowedOrigins; } /** * Variant of {@link #setAllowedOrigins} for adding one origin at a time. */ public void addAllowedOrigin(@Nullable String origin) { if (origin == null) { return; } if (this.allowedOrigins == null) { this.allowedOrigins = new ArrayList<>(4); } else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { setAllowedOrigins(DEFAULT_PERMIT_ALL); } parseCommaDelimitedOrigin(origin, value -> { value = trimTrailingSlash(value); this.allowedOrigins.add(value); }); } /** * Alternative to {@link #setAllowedOrigins} that supports more flexible * origins patterns with "*" anywhere in the host name in addition to port * lists. Examples: *

    *
  • {@literal https://*.domain1.com} -- domains ending with domain1.com *
  • {@literal https://*.domain1.com:[8080,8081]} -- domains ending with * domain1.com on port 8080 or port 8081 *
  • {@literal https://*.domain1.com:[*]} -- domains ending with * domain1.com on any port, including the default port *
  • comma-delimited list of patters, e.g. * {@code "https://*.a1.com,https://*.a2.com"}; this is convenient when a * value is resolved through a property placeholder, e.g. {@code "${origin}"}; * note that such placeholders must be resolved externally. *
*

In contrast to {@link #setAllowedOrigins(List) allowedOrigins} which * only supports "*" and cannot be used with {@code allowCredentials}, when * an allowedOriginPattern is matched, the {@code Access-Control-Allow-Origin} * response header is set to the matched origin and not to {@code "*"} nor * to the pattern. Therefore, allowedOriginPatterns can be used in combination * with {@link #setAllowCredentials} set to {@code true}. *

By default this is not set. * @since 5.3 */ public CorsConfiguration setAllowedOriginPatterns(@Nullable List allowedOriginPatterns) { if (allowedOriginPatterns == null) { this.allowedOriginPatterns = null; } else { this.allowedOriginPatterns = new ArrayList<>(allowedOriginPatterns.size()); for (String patternValue : allowedOriginPatterns) { addAllowedOriginPattern(patternValue); } } return this; } /** * Return the configured origins patterns to allow, or {@code null} if none. * @since 5.3 */ @Nullable public List getAllowedOriginPatterns() { if (this.allowedOriginPatterns == null) { return null; } return this.allowedOriginPatterns.stream() .map(OriginPattern::getDeclaredPattern) .toList(); } /** * Variant of {@link #setAllowedOriginPatterns} for adding one origin at a time. * @since 5.3 */ public void addAllowedOriginPattern(@Nullable String originPattern) { if (originPattern == null) { return; } if (this.allowedOriginPatterns == null) { this.allowedOriginPatterns = new ArrayList<>(4); } parseCommaDelimitedOrigin(originPattern, value -> { value = trimTrailingSlash(value); this.allowedOriginPatterns.add(new OriginPattern(value)); if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { this.allowedOrigins = null; } }); } private static void parseCommaDelimitedOrigin(String rawValue, Consumer valueConsumer) { if (rawValue.indexOf(',') == -1) { valueConsumer.accept(rawValue); return; } int start = 0; boolean withinPortRange = false; for (int current = 0; current < rawValue.length(); current++) { switch (rawValue.charAt(current)) { case '[': withinPortRange = true; break; case ']': withinPortRange = false; break; case ',': if (!withinPortRange) { valueConsumer.accept(rawValue.substring(start, current).trim()); start = current + 1; } break; } } if (start < rawValue.length()) { valueConsumer.accept(rawValue.substring(start)); } } /** * Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, * {@code "PUT"}, etc. *

The special value {@code "*"} allows all methods. *

If not set, only {@code "GET"} and {@code "HEAD"} are allowed. *

By default this is not set. *

Note: CORS checks use values from "Forwarded" * (RFC 7239), * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers, * if present, in order to reflect the client-originated address. * Consider using the {@code ForwardedHeaderFilter} in order to choose from a * central place whether to extract and use, or to discard such headers. * See the Spring Framework reference for more on this filter. */ public void setAllowedMethods(@Nullable List allowedMethods) { this.allowedMethods = (allowedMethods != null ? new ArrayList<>(allowedMethods) : null); if (!CollectionUtils.isEmpty(allowedMethods)) { this.resolvedMethods = new ArrayList<>(allowedMethods.size()); for (String method : allowedMethods) { if (ALL.equals(method)) { this.resolvedMethods = null; break; } this.resolvedMethods.add(HttpMethod.valueOf(method)); } } else { this.resolvedMethods = DEFAULT_METHODS; } } /** * Return the allowed HTTP methods, or {@code null} in which case * only {@code "GET"} and {@code "HEAD"} allowed. * @see #addAllowedMethod(HttpMethod) * @see #addAllowedMethod(String) * @see #setAllowedMethods(List) */ @Nullable public List getAllowedMethods() { return this.allowedMethods; } /** * Add an HTTP method to allow. */ public void addAllowedMethod(HttpMethod method) { addAllowedMethod(method.name()); } /** * Add an HTTP method to allow. */ public void addAllowedMethod(String method) { if (StringUtils.hasText(method)) { if (this.allowedMethods == null) { this.allowedMethods = new ArrayList<>(4); this.resolvedMethods = new ArrayList<>(4); } else if (this.allowedMethods == DEFAULT_PERMIT_METHODS) { setAllowedMethods(DEFAULT_PERMIT_METHODS); } this.allowedMethods.add(method); if (ALL.equals(method)) { this.resolvedMethods = null; } else if (this.resolvedMethods != null) { this.resolvedMethods.add(HttpMethod.valueOf(method)); } } } /** * Set the list of headers that a pre-flight request can list as allowed * for use during an actual request. *

The special value {@code "*"} allows actual requests to send any * header. *

A header name is not required to be listed if it is one of: * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, * {@code Last-Modified}, or {@code Pragma}. *

By default this is not set. */ public void setAllowedHeaders(@Nullable List allowedHeaders) { this.allowedHeaders = (allowedHeaders != null ? new ArrayList<>(allowedHeaders) : null); } /** * Return the allowed actual request headers, or {@code null} if none. * @see #addAllowedHeader(String) * @see #setAllowedHeaders(List) */ @Nullable public List getAllowedHeaders() { return this.allowedHeaders; } /** * Add an actual request header to allow. */ public void addAllowedHeader(String allowedHeader) { if (this.allowedHeaders == null) { this.allowedHeaders = new ArrayList<>(4); } else if (this.allowedHeaders == DEFAULT_PERMIT_ALL) { setAllowedHeaders(DEFAULT_PERMIT_ALL); } this.allowedHeaders.add(allowedHeader); } /** * Set the list of response headers other than simple headers (i.e. * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, * {@code Expires}, {@code Last-Modified}, or {@code Pragma}) that an * actual response might have and can be exposed. *

The special value {@code "*"} allows all headers to be exposed for * non-credentialed requests. *

By default this is not set. */ public void setExposedHeaders(@Nullable List exposedHeaders) { this.exposedHeaders = (exposedHeaders != null ? new ArrayList<>(exposedHeaders) : null); } /** * Return the configured response headers to expose, or {@code null} if none. * @see #addExposedHeader(String) * @see #setExposedHeaders(List) */ @Nullable public List getExposedHeaders() { return this.exposedHeaders; } /** * Add a response header to expose. *

The special value {@code "*"} allows all headers to be exposed for * non-credentialed requests. */ public void addExposedHeader(String exposedHeader) { if (this.exposedHeaders == null) { this.exposedHeaders = new ArrayList<>(4); } this.exposedHeaders.add(exposedHeader); } /** * Whether user credentials are supported. *

By default this is not set (i.e. user credentials are not supported). */ public void setAllowCredentials(@Nullable Boolean allowCredentials) { this.allowCredentials = allowCredentials; } /** * Return the configured {@code allowCredentials} flag, or {@code null} if none. * @see #setAllowCredentials(Boolean) */ @Nullable public Boolean getAllowCredentials() { return this.allowCredentials; } /** * Configure how long, as a duration, the response from a pre-flight request * can be cached by clients. * @since 5.2 * @see #setMaxAge(Long) */ public void setMaxAge(Duration maxAge) { this.maxAge = maxAge.getSeconds(); } /** * Configure how long, in seconds, the response from a pre-flight request * can be cached by clients. *

By default this is not set. */ public void setMaxAge(@Nullable Long maxAge) { this.maxAge = maxAge; } /** * Return the configured {@code maxAge} value, or {@code null} if none. * @see #setMaxAge(Long) */ @Nullable public Long getMaxAge() { return this.maxAge; } /** * By default {@code CorsConfiguration} does not permit any cross-origin * requests and must be configured explicitly. Use this method to switch to * defaults that permit all cross-origin requests for GET, HEAD, and POST, * but not overriding any values that have already been set. *

The following defaults are applied for values that are not set: *

    *
  • Allow all origins with the special value {@code "*"} defined in the * CORS spec. This is set only if neither {@link #setAllowedOrigins origins} * nor {@link #setAllowedOriginPatterns originPatterns} are already set.
  • *
  • Allow "simple" methods {@code GET}, {@code HEAD} and {@code POST}.
  • *
  • Allow all headers.
  • *
  • Set max age to 1800 seconds (30 minutes).
  • *
*/ public CorsConfiguration applyPermitDefaultValues() { if (this.allowedOrigins == null && this.allowedOriginPatterns == null) { this.allowedOrigins = DEFAULT_PERMIT_ALL; } if (this.allowedMethods == null) { this.allowedMethods = DEFAULT_PERMIT_METHODS; this.resolvedMethods = DEFAULT_PERMIT_METHODS .stream().map(HttpMethod::valueOf).toList(); } if (this.allowedHeaders == null) { this.allowedHeaders = DEFAULT_PERMIT_ALL; } if (this.maxAge == null) { this.maxAge = 1800L; } return this; } /** * Validate that when {@link #setAllowCredentials allowCredentials} is {@code true}, * {@link #setAllowedOrigins allowedOrigins} does not contain the special * value {@code "*"} since in that case the "Access-Control-Allow-Origin" * cannot be set to {@code "*"}. * @throws IllegalArgumentException if the validation fails * @since 5.3 */ public void validateAllowCredentials() { if (this.allowCredentials == Boolean.TRUE && this.allowedOrigins != null && this.allowedOrigins.contains(ALL)) { throw new IllegalArgumentException( "When allowCredentials is true, allowedOrigins cannot contain the special value \"*\" " + "since that cannot be set on the \"Access-Control-Allow-Origin\" response header. " + "To allow credentials to a set of origins, list them explicitly " + "or consider using \"allowedOriginPatterns\" instead."); } } /** * Combine the non-null properties of the supplied * {@code CorsConfiguration} with this one. *

When combining single values like {@code allowCredentials} or * {@code maxAge}, {@code this} properties are overridden by non-null * {@code other} properties if any. *

Combining lists like {@code allowedOrigins}, {@code allowedMethods}, * {@code allowedHeaders} or {@code exposedHeaders} is done in an additive * way. For example, combining {@code ["GET", "POST"]} with * {@code ["PATCH"]} results in {@code ["GET", "POST", "PATCH"]}. However, * combining {@code ["GET", "POST"]} with {@code ["*"]} results in * {@code ["*"]}. Note also that default permit values set by * {@link CorsConfiguration#applyPermitDefaultValues()} are overridden by * any explicitly defined values. * @return the combined {@code CorsConfiguration}, or {@code this} * configuration if the supplied configuration is {@code null} */ public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (other == null) { return this; } // Bypass setAllowedOrigins to avoid re-compiling patterns CorsConfiguration config = new CorsConfiguration(this); List origins = combine(getAllowedOrigins(), other.getAllowedOrigins()); List patterns = combinePatterns(this.allowedOriginPatterns, other.allowedOriginPatterns); config.allowedOrigins = (origins == DEFAULT_PERMIT_ALL && !CollectionUtils.isEmpty(patterns) ? null : origins); config.allowedOriginPatterns = patterns; config.setAllowedMethods(combine(getAllowedMethods(), other.getAllowedMethods())); config.setAllowedHeaders(combine(getAllowedHeaders(), other.getAllowedHeaders())); config.setExposedHeaders(combine(getExposedHeaders(), other.getExposedHeaders())); Boolean allowCredentials = other.getAllowCredentials(); if (allowCredentials != null) { config.setAllowCredentials(allowCredentials); } Long maxAge = other.getMaxAge(); if (maxAge != null) { config.setMaxAge(maxAge); } return config; } private List combine(@Nullable List source, @Nullable List other) { if (other == null) { return (source != null ? source : Collections.emptyList()); } if (source == null) { return other; } if (source == DEFAULT_PERMIT_ALL || source == DEFAULT_PERMIT_METHODS) { return other; } if (other == DEFAULT_PERMIT_ALL || other == DEFAULT_PERMIT_METHODS) { return source; } if (source.contains(ALL) || other.contains(ALL)) { return ALL_LIST; } Set combined = new LinkedHashSet<>(source.size() + other.size()); combined.addAll(source); combined.addAll(other); return new ArrayList<>(combined); } private List combinePatterns( @Nullable List source, @Nullable List other) { if (other == null) { return (source != null ? source : Collections.emptyList()); } if (source == null) { return other; } if (source.contains(ALL_PATTERN) || other.contains(ALL_PATTERN)) { return ALL_PATTERN_LIST; } Set combined = new LinkedHashSet<>(source.size() + other.size()); combined.addAll(source); combined.addAll(other); return new ArrayList<>(combined); } /** * Check the origin of the request against the configured allowed origins. * @param origin the origin to check * @return the origin to use for the response, or {@code null} which * means the request origin is not allowed */ @Nullable public String checkOrigin(@Nullable String origin) { if (!StringUtils.hasText(origin)) { return null; } String originToCheck = trimTrailingSlash(origin); if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { if (originToCheck.equalsIgnoreCase(allowedOrigin)) { return origin; } } } if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { for (OriginPattern p : this.allowedOriginPatterns) { if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(originToCheck).matches()) { return origin; } } } return null; } /** * Check the HTTP request method (or the method from the * {@code Access-Control-Request-Method} header on a pre-flight request) * against the configured allowed methods. * @param requestMethod the HTTP request method to check * @return the list of HTTP methods to list in the response of a pre-flight * request, or {@code null} if the supplied {@code requestMethod} is not allowed */ @Nullable public List checkHttpMethod(@Nullable HttpMethod requestMethod) { if (requestMethod == null) { return null; } if (this.resolvedMethods == null) { return Collections.singletonList(requestMethod); } return (this.resolvedMethods.contains(requestMethod) ? this.resolvedMethods : null); } /** * Check the supplied request headers (or the headers listed in the * {@code Access-Control-Request-Headers} of a pre-flight request) against * the configured allowed headers. * @param requestHeaders the request headers to check * @return the list of allowed headers to list in the response of a pre-flight * request, or {@code null} if none of the supplied request headers is allowed */ @Nullable public List checkHeaders(@Nullable List requestHeaders) { if (requestHeaders == null) { return null; } if (requestHeaders.isEmpty()) { return Collections.emptyList(); } if (ObjectUtils.isEmpty(this.allowedHeaders)) { return null; } boolean allowAnyHeader = this.allowedHeaders.contains(ALL); List result = new ArrayList<>(requestHeaders.size()); for (String requestHeader : requestHeaders) { if (StringUtils.hasText(requestHeader)) { requestHeader = requestHeader.trim(); if (allowAnyHeader) { result.add(requestHeader); } else { for (String allowedHeader : this.allowedHeaders) { if (requestHeader.equalsIgnoreCase(allowedHeader)) { result.add(requestHeader); break; } } } } } return (result.isEmpty() ? null : result); } /** * Contains both the user-declared pattern (e.g. "https://*.domain.com") and * the regex {@link Pattern} derived from it. */ private static class OriginPattern { private static final Pattern PORTS_PATTERN = Pattern.compile("(.*):\\[(\\*|\\d+(,\\d+)*)]"); private final String declaredPattern; private final Pattern pattern; OriginPattern(String declaredPattern) { this.declaredPattern = declaredPattern; this.pattern = initPattern(declaredPattern); } private static Pattern initPattern(String patternValue) { String portList = null; Matcher matcher = PORTS_PATTERN.matcher(patternValue); if (matcher.matches()) { patternValue = matcher.group(1); portList = matcher.group(2); } patternValue = "\\Q" + patternValue + "\\E"; patternValue = patternValue.replace("*", "\\E.*\\Q"); if (portList != null) { patternValue += (portList.equals(ALL) ? "(:\\d+)?" : ":(" + portList.replace(',', '|') + ")"); } return Pattern.compile(patternValue); } public String getDeclaredPattern() { return this.declaredPattern; } public Pattern getPattern() { return this.pattern; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (other == null || !getClass().equals(other.getClass())) { return false; } return ObjectUtils.nullSafeEquals( this.declaredPattern, ((OriginPattern) other).declaredPattern); } @Override public int hashCode() { return this.declaredPattern.hashCode(); } @Override public String toString() { return this.declaredPattern; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy