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