001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.cors; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collections; 022import java.util.LinkedHashSet; 023import java.util.List; 024import java.util.Set; 025 026import org.springframework.http.HttpMethod; 027import org.springframework.util.CollectionUtils; 028import org.springframework.util.ObjectUtils; 029import org.springframework.util.StringUtils; 030 031/** 032 * A container for CORS configuration along with methods to check against the 033 * actual origin, HTTP methods, and headers of a given request. 034 * 035 * <p>By default a newly created {@code CorsConfiguration} does not permit any 036 * cross-origin requests and must be configured explicitly to indicate what 037 * should be allowed. Use {@link #applyPermitDefaultValues()} to flip the 038 * initialization model to start with open defaults that permit all cross-origin 039 * requests for GET, HEAD, and POST requests. 040 * 041 * @author Sebastien Deleuze 042 * @author Rossen Stoyanchev 043 * @author Juergen Hoeller 044 * @author Sam Brannen 045 * @since 4.2 046 * @see <a href="https://www.w3.org/TR/cors/">CORS spec</a> 047 */ 048public class CorsConfiguration { 049 050 /** Wildcard representing <em>all</em> origins, methods, or headers. */ 051 public static final String ALL = "*"; 052 053 private static final List<HttpMethod> DEFAULT_METHODS; 054 055 static { 056 List<HttpMethod> rawMethods = new ArrayList<HttpMethod>(2); 057 rawMethods.add(HttpMethod.GET); 058 rawMethods.add(HttpMethod.HEAD); 059 DEFAULT_METHODS = Collections.unmodifiableList(rawMethods); 060 } 061 062 063 private List<String> allowedOrigins; 064 065 private List<String> allowedMethods; 066 067 private List<HttpMethod> resolvedMethods = DEFAULT_METHODS; 068 069 private List<String> allowedHeaders; 070 071 private List<String> exposedHeaders; 072 073 private Boolean allowCredentials; 074 075 private Long maxAge; 076 077 078 /** 079 * Construct a new {@code CorsConfiguration} instance with no cross-origin 080 * requests allowed for any origin by default. 081 * @see #applyPermitDefaultValues() 082 */ 083 public CorsConfiguration() { 084 } 085 086 /** 087 * Construct a new {@code CorsConfiguration} instance by copying all 088 * values from the supplied {@code CorsConfiguration}. 089 */ 090 public CorsConfiguration(CorsConfiguration other) { 091 this.allowedOrigins = other.allowedOrigins; 092 this.allowedMethods = other.allowedMethods; 093 this.resolvedMethods = other.resolvedMethods; 094 this.allowedHeaders = other.allowedHeaders; 095 this.exposedHeaders = other.exposedHeaders; 096 this.allowCredentials = other.allowCredentials; 097 this.maxAge = other.maxAge; 098 } 099 100 101 /** 102 * Set the origins to allow, e.g. {@code "https://domain1.com"}. 103 * <p>The special value {@code "*"} allows all domains. 104 * <p>By default this is not set. 105 * <p><strong>Note:</strong> CORS checks use values from "Forwarded" 106 * (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>), 107 * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers, 108 * if present, in order to reflect the client-originated address. 109 * Consider using the {@code ForwardedHeaderFilter} in order to choose from a 110 * central place whether to extract and use, or to discard such headers. 111 * See the Spring Framework reference for more on this filter. 112 */ 113 public void setAllowedOrigins(List<String> allowedOrigins) { 114 this.allowedOrigins = (allowedOrigins != null ? new ArrayList<String>(allowedOrigins) : null); 115 } 116 117 /** 118 * Return the configured origins to allow, or {@code null} if none. 119 * @see #addAllowedOrigin(String) 120 * @see #setAllowedOrigins(List) 121 */ 122 public List<String> getAllowedOrigins() { 123 return this.allowedOrigins; 124 } 125 126 /** 127 * Add an origin to allow. 128 */ 129 public void addAllowedOrigin(String origin) { 130 if (this.allowedOrigins == null) { 131 this.allowedOrigins = new ArrayList<String>(4); 132 } 133 this.allowedOrigins.add(origin); 134 } 135 136 /** 137 * Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, 138 * {@code "PUT"}, etc. 139 * <p>The special value {@code "*"} allows all methods. 140 * <p>If not set, only {@code "GET"} and {@code "HEAD"} are allowed. 141 * <p>By default this is not set. 142 */ 143 public void setAllowedMethods(List<String> allowedMethods) { 144 this.allowedMethods = (allowedMethods != null ? new ArrayList<String>(allowedMethods) : null); 145 if (!CollectionUtils.isEmpty(allowedMethods)) { 146 this.resolvedMethods = new ArrayList<HttpMethod>(allowedMethods.size()); 147 for (String method : allowedMethods) { 148 if (ALL.equals(method)) { 149 this.resolvedMethods = null; 150 break; 151 } 152 this.resolvedMethods.add(HttpMethod.resolve(method)); 153 } 154 } 155 else { 156 this.resolvedMethods = DEFAULT_METHODS; 157 } 158 } 159 160 /** 161 * Return the allowed HTTP methods, or {@code null} in which case 162 * only {@code "GET"} and {@code "HEAD"} allowed. 163 * @see #addAllowedMethod(HttpMethod) 164 * @see #addAllowedMethod(String) 165 * @see #setAllowedMethods(List) 166 */ 167 public List<String> getAllowedMethods() { 168 return this.allowedMethods; 169 } 170 171 /** 172 * Add an HTTP method to allow. 173 */ 174 public void addAllowedMethod(HttpMethod method) { 175 if (method != null) { 176 addAllowedMethod(method.name()); 177 } 178 } 179 180 /** 181 * Add an HTTP method to allow. 182 */ 183 public void addAllowedMethod(String method) { 184 if (StringUtils.hasText(method)) { 185 if (this.allowedMethods == null) { 186 this.allowedMethods = new ArrayList<String>(4); 187 this.resolvedMethods = new ArrayList<HttpMethod>(4); 188 } 189 this.allowedMethods.add(method); 190 if (ALL.equals(method)) { 191 this.resolvedMethods = null; 192 } 193 else if (this.resolvedMethods != null) { 194 this.resolvedMethods.add(HttpMethod.resolve(method)); 195 } 196 } 197 } 198 199 /** 200 * Set the list of headers that a pre-flight request can list as allowed 201 * for use during an actual request. 202 * <p>The special value {@code "*"} allows actual requests to send any 203 * header. 204 * <p>A header name is not required to be listed if it is one of: 205 * {@code Cache-Control}, {@code Content-Language}, {@code Expires}, 206 * {@code Last-Modified}, or {@code Pragma}. 207 * <p>By default this is not set. 208 */ 209 public void setAllowedHeaders(List<String> allowedHeaders) { 210 this.allowedHeaders = (allowedHeaders != null ? new ArrayList<String>(allowedHeaders) : null); 211 } 212 213 /** 214 * Return the allowed actual request headers, or {@code null} if none. 215 * @see #addAllowedHeader(String) 216 * @see #setAllowedHeaders(List) 217 */ 218 public List<String> getAllowedHeaders() { 219 return this.allowedHeaders; 220 } 221 222 /** 223 * Add an actual request header to allow. 224 */ 225 public void addAllowedHeader(String allowedHeader) { 226 if (this.allowedHeaders == null) { 227 this.allowedHeaders = new ArrayList<String>(4); 228 } 229 this.allowedHeaders.add(allowedHeader); 230 } 231 232 /** 233 * Set the list of response headers other than simple headers (i.e. 234 * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, 235 * {@code Expires}, {@code Last-Modified}, or {@code Pragma}) that an 236 * actual response might have and can be exposed. 237 * <p>Note that {@code "*"} is not a valid exposed header value. 238 * <p>By default this is not set. 239 */ 240 public void setExposedHeaders(List<String> exposedHeaders) { 241 if (exposedHeaders != null && exposedHeaders.contains(ALL)) { 242 throw new IllegalArgumentException("'*' is not a valid exposed header value"); 243 } 244 this.exposedHeaders = (exposedHeaders != null ? new ArrayList<String>(exposedHeaders) : null); 245 } 246 247 /** 248 * Return the configured response headers to expose, or {@code null} if none. 249 * @see #addExposedHeader(String) 250 * @see #setExposedHeaders(List) 251 */ 252 public List<String> getExposedHeaders() { 253 return this.exposedHeaders; 254 } 255 256 /** 257 * Add a response header to expose. 258 * <p>Note that {@code "*"} is not a valid exposed header value. 259 */ 260 public void addExposedHeader(String exposedHeader) { 261 if (ALL.equals(exposedHeader)) { 262 throw new IllegalArgumentException("'*' is not a valid exposed header value"); 263 } 264 if (this.exposedHeaders == null) { 265 this.exposedHeaders = new ArrayList<String>(4); 266 } 267 this.exposedHeaders.add(exposedHeader); 268 } 269 270 /** 271 * Whether user credentials are supported. 272 * <p>By default this is not set (i.e. user credentials are not supported). 273 */ 274 public void setAllowCredentials(Boolean allowCredentials) { 275 this.allowCredentials = allowCredentials; 276 } 277 278 /** 279 * Return the configured {@code allowCredentials} flag, or {@code null} if none. 280 * @see #setAllowCredentials(Boolean) 281 */ 282 public Boolean getAllowCredentials() { 283 return this.allowCredentials; 284 } 285 286 /** 287 * Configure how long, in seconds, the response from a pre-flight request 288 * can be cached by clients. 289 * <p>By default this is not set. 290 */ 291 public void setMaxAge(Long maxAge) { 292 this.maxAge = maxAge; 293 } 294 295 /** 296 * Return the configured {@code maxAge} value, or {@code null} if none. 297 * @see #setMaxAge(Long) 298 */ 299 public Long getMaxAge() { 300 return this.maxAge; 301 } 302 303 304 /** 305 * By default a newly created {@code CorsConfiguration} does not permit any 306 * cross-origin requests and must be configured explicitly to indicate what 307 * should be allowed. 308 * <p>Use this method to flip the initialization model to start with open 309 * defaults that permit all cross-origin requests for GET, HEAD, and POST 310 * requests. Note however that this method will not override any existing 311 * values already set. 312 * <p>The following defaults are applied if not already set: 313 * <ul> 314 * <li>Allow all origins.</li> 315 * <li>Allow "simple" methods {@code GET}, {@code HEAD} and {@code POST}.</li> 316 * <li>Allow all headers.</li> 317 * <li>Set max age to 1800 seconds (30 minutes).</li> 318 * </ul> 319 */ 320 public CorsConfiguration applyPermitDefaultValues() { 321 if (this.allowedOrigins == null) { 322 this.addAllowedOrigin(ALL); 323 } 324 if (this.allowedMethods == null) { 325 this.setAllowedMethods(Arrays.asList( 326 HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name())); 327 } 328 if (this.allowedHeaders == null) { 329 this.addAllowedHeader(ALL); 330 } 331 if (this.maxAge == null) { 332 this.setMaxAge(1800L); 333 } 334 return this; 335 } 336 337 /** 338 * Combine the supplied {@code CorsConfiguration} with this one. 339 * <p>Properties of this configuration are overridden by any non-null 340 * properties of the supplied one. 341 * @return the combined {@code CorsConfiguration} or {@code this} 342 * configuration if the supplied configuration is {@code null} 343 */ 344 public CorsConfiguration combine(CorsConfiguration other) { 345 if (other == null) { 346 return this; 347 } 348 CorsConfiguration config = new CorsConfiguration(this); 349 config.setAllowedOrigins(combine(getAllowedOrigins(), other.getAllowedOrigins())); 350 config.setAllowedMethods(combine(getAllowedMethods(), other.getAllowedMethods())); 351 config.setAllowedHeaders(combine(getAllowedHeaders(), other.getAllowedHeaders())); 352 config.setExposedHeaders(combine(getExposedHeaders(), other.getExposedHeaders())); 353 Boolean allowCredentials = other.getAllowCredentials(); 354 if (allowCredentials != null) { 355 config.setAllowCredentials(allowCredentials); 356 } 357 Long maxAge = other.getMaxAge(); 358 if (maxAge != null) { 359 config.setMaxAge(maxAge); 360 } 361 return config; 362 } 363 364 private List<String> combine(List<String> source, List<String> other) { 365 if (other == null || other.contains(ALL)) { 366 return source; 367 } 368 if (source == null || source.contains(ALL)) { 369 return other; 370 } 371 Set<String> combined = new LinkedHashSet<String>(source); 372 combined.addAll(other); 373 return new ArrayList<String>(combined); 374 } 375 376 /** 377 * Check the origin of the request against the configured allowed origins. 378 * @param requestOrigin the origin to check 379 * @return the origin to use for the response, or {@code null} which 380 * means the request origin is not allowed 381 */ 382 public String checkOrigin(String requestOrigin) { 383 if (!StringUtils.hasText(requestOrigin)) { 384 return null; 385 } 386 if (ObjectUtils.isEmpty(this.allowedOrigins)) { 387 return null; 388 } 389 390 if (this.allowedOrigins.contains(ALL)) { 391 if (this.allowCredentials != Boolean.TRUE) { 392 return ALL; 393 } 394 else { 395 return requestOrigin; 396 } 397 } 398 for (String allowedOrigin : this.allowedOrigins) { 399 if (requestOrigin.equalsIgnoreCase(allowedOrigin)) { 400 return requestOrigin; 401 } 402 } 403 404 return null; 405 } 406 407 /** 408 * Check the HTTP request method (or the method from the 409 * {@code Access-Control-Request-Method} header on a pre-flight request) 410 * against the configured allowed methods. 411 * @param requestMethod the HTTP request method to check 412 * @return the list of HTTP methods to list in the response of a pre-flight 413 * request, or {@code null} if the supplied {@code requestMethod} is not allowed 414 */ 415 public List<HttpMethod> checkHttpMethod(HttpMethod requestMethod) { 416 if (requestMethod == null) { 417 return null; 418 } 419 if (this.resolvedMethods == null) { 420 return Collections.singletonList(requestMethod); 421 } 422 return (this.resolvedMethods.contains(requestMethod) ? this.resolvedMethods : null); 423 } 424 425 /** 426 * Check the supplied request headers (or the headers listed in the 427 * {@code Access-Control-Request-Headers} of a pre-flight request) against 428 * the configured allowed headers. 429 * @param requestHeaders the request headers to check 430 * @return the list of allowed headers to list in the response of a pre-flight 431 * request, or {@code null} if none of the supplied request headers is allowed 432 */ 433 public List<String> checkHeaders(List<String> requestHeaders) { 434 if (requestHeaders == null) { 435 return null; 436 } 437 if (requestHeaders.isEmpty()) { 438 return Collections.emptyList(); 439 } 440 if (ObjectUtils.isEmpty(this.allowedHeaders)) { 441 return null; 442 } 443 444 boolean allowAnyHeader = this.allowedHeaders.contains(ALL); 445 List<String> result = new ArrayList<String>(requestHeaders.size()); 446 for (String requestHeader : requestHeaders) { 447 if (StringUtils.hasText(requestHeader)) { 448 requestHeader = requestHeader.trim(); 449 if (allowAnyHeader) { 450 result.add(requestHeader); 451 } 452 else { 453 for (String allowedHeader : this.allowedHeaders) { 454 if (requestHeader.equalsIgnoreCase(allowedHeader)) { 455 result.add(requestHeader); 456 break; 457 } 458 } 459 } 460 } 461 } 462 return (result.isEmpty() ? null : result); 463 } 464 465}