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.reactive; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.List; 022 023import org.apache.commons.logging.Log; 024import org.apache.commons.logging.LogFactory; 025 026import org.springframework.http.HttpHeaders; 027import org.springframework.http.HttpMethod; 028import org.springframework.http.HttpStatus; 029import org.springframework.http.server.reactive.ServerHttpRequest; 030import org.springframework.http.server.reactive.ServerHttpResponse; 031import org.springframework.lang.Nullable; 032import org.springframework.util.CollectionUtils; 033import org.springframework.web.cors.CorsConfiguration; 034import org.springframework.web.server.ServerWebExchange; 035 036/** 037 * The default implementation of {@link CorsProcessor}, 038 * as defined by the <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>. 039 * 040 * <p>Note that when input {@link CorsConfiguration} is {@code null}, this 041 * implementation does not reject simple or actual requests outright but simply 042 * avoid adding CORS headers to the response. CORS processing is also skipped 043 * if the response already contains CORS headers. 044 * 045 * @author Sebastien Deleuze 046 * @author Rossen Stoyanchev 047 * @since 5.0 048 */ 049public class DefaultCorsProcessor implements CorsProcessor { 050 051 private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class); 052 053 private static final List<String> VARY_HEADERS = Arrays.asList( 054 HttpHeaders.ORIGIN, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); 055 056 057 @Override 058 public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) { 059 060 ServerHttpRequest request = exchange.getRequest(); 061 ServerHttpResponse response = exchange.getResponse(); 062 HttpHeaders responseHeaders = response.getHeaders(); 063 064 List<String> varyHeaders = responseHeaders.get(HttpHeaders.VARY); 065 if (varyHeaders == null) { 066 responseHeaders.addAll(HttpHeaders.VARY, VARY_HEADERS); 067 } 068 else { 069 for (String header : VARY_HEADERS) { 070 if (!varyHeaders.contains(header)) { 071 responseHeaders.add(HttpHeaders.VARY, header); 072 } 073 } 074 } 075 076 if (!CorsUtils.isCorsRequest(request)) { 077 return true; 078 } 079 080 if (responseHeaders.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) { 081 logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\""); 082 return true; 083 } 084 085 boolean preFlightRequest = CorsUtils.isPreFlightRequest(request); 086 if (config == null) { 087 if (preFlightRequest) { 088 rejectRequest(response); 089 return false; 090 } 091 else { 092 return true; 093 } 094 } 095 096 return handleInternal(exchange, config, preFlightRequest); 097 } 098 099 /** 100 * Invoked when one of the CORS checks failed. 101 */ 102 protected void rejectRequest(ServerHttpResponse response) { 103 response.setStatusCode(HttpStatus.FORBIDDEN); 104 } 105 106 /** 107 * Handle the given request. 108 */ 109 protected boolean handleInternal(ServerWebExchange exchange, 110 CorsConfiguration config, boolean preFlightRequest) { 111 112 ServerHttpRequest request = exchange.getRequest(); 113 ServerHttpResponse response = exchange.getResponse(); 114 HttpHeaders responseHeaders = response.getHeaders(); 115 116 String requestOrigin = request.getHeaders().getOrigin(); 117 String allowOrigin = checkOrigin(config, requestOrigin); 118 if (allowOrigin == null) { 119 logger.debug("Reject: '" + requestOrigin + "' origin is not allowed"); 120 rejectRequest(response); 121 return false; 122 } 123 124 HttpMethod requestMethod = getMethodToUse(request, preFlightRequest); 125 List<HttpMethod> allowMethods = checkMethods(config, requestMethod); 126 if (allowMethods == null) { 127 logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed"); 128 rejectRequest(response); 129 return false; 130 } 131 132 List<String> requestHeaders = getHeadersToUse(request, preFlightRequest); 133 List<String> allowHeaders = checkHeaders(config, requestHeaders); 134 if (preFlightRequest && allowHeaders == null) { 135 logger.debug("Reject: headers '" + requestHeaders + "' are not allowed"); 136 rejectRequest(response); 137 return false; 138 } 139 140 responseHeaders.setAccessControlAllowOrigin(allowOrigin); 141 142 if (preFlightRequest) { 143 responseHeaders.setAccessControlAllowMethods(allowMethods); 144 } 145 146 if (preFlightRequest && !allowHeaders.isEmpty()) { 147 responseHeaders.setAccessControlAllowHeaders(allowHeaders); 148 } 149 150 if (!CollectionUtils.isEmpty(config.getExposedHeaders())) { 151 responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders()); 152 } 153 154 if (Boolean.TRUE.equals(config.getAllowCredentials())) { 155 responseHeaders.setAccessControlAllowCredentials(true); 156 } 157 158 if (preFlightRequest && config.getMaxAge() != null) { 159 responseHeaders.setAccessControlMaxAge(config.getMaxAge()); 160 } 161 162 return true; 163 } 164 165 /** 166 * Check the origin and determine the origin for the response. The default 167 * implementation simply delegates to 168 * {@link CorsConfiguration#checkOrigin(String)}. 169 */ 170 @Nullable 171 protected String checkOrigin(CorsConfiguration config, @Nullable String requestOrigin) { 172 return config.checkOrigin(requestOrigin); 173 } 174 175 /** 176 * Check the HTTP method and determine the methods for the response of a 177 * pre-flight request. The default implementation simply delegates to 178 * {@link CorsConfiguration#checkHttpMethod(HttpMethod)}. 179 */ 180 @Nullable 181 protected List<HttpMethod> checkMethods(CorsConfiguration config, @Nullable HttpMethod requestMethod) { 182 return config.checkHttpMethod(requestMethod); 183 } 184 185 @Nullable 186 private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight) { 187 return (isPreFlight ? request.getHeaders().getAccessControlRequestMethod() : request.getMethod()); 188 } 189 190 /** 191 * Check the headers and determine the headers for the response of a 192 * pre-flight request. The default implementation simply delegates to 193 * {@link CorsConfiguration#checkOrigin(String)}. 194 */ 195 @Nullable 196 197 protected List<String> checkHeaders(CorsConfiguration config, List<String> requestHeaders) { 198 return config.checkHeaders(requestHeaders); 199 } 200 201 private List<String> getHeadersToUse(ServerHttpRequest request, boolean isPreFlight) { 202 HttpHeaders headers = request.getHeaders(); 203 return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<>(headers.keySet())); 204 } 205 206}