CORS

Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section describes how to do so.

Introduction

For security reasons, browsers prohibit AJAX calls to resources outside the current origin. For example, you could have your bank account in one tab and evil.com in another. Scripts from evil.com should not be able to make AJAX requests to your bank API with your credentials — for example, withdrawing money from your account!

Cross-Origin Resource Sharing (CORS) is a W3C specification implemented by most browsers that lets you specify what kind of cross-domain requests are authorized, rather than using less secure and less powerful workarounds based on IFRAME or JSONP.

Processing

The CORS specification distinguishes between preflight, simple, and actual requests. To learn how CORS works, you can read this article, among many others, or see the specification for more details.

Spring WebFlux HandlerMapping implementations provide built-in support for CORS. After successfully mapping a request to a handler, a HandlerMapping checks the CORS configuration for the given request and handler and takes further actions. Preflight requests are handled directly, while simple and actual CORS requests are intercepted, validated, and have the required CORS response headers set.

In order to enable cross-origin requests (that is, the Origin header is present and differs from the host of the request), you need to have some explicitly declared CORS configuration. If no matching CORS configuration is found, preflight requests are rejected. No CORS headers are added to the responses of simple and actual CORS requests and, consequently, browsers reject them.

Each HandlerMapping can be configured individually with URL pattern-based CorsConfiguration mappings. In most cases, applications use the WebFlux Java configuration to declare such mappings, which results in a single, global map passed to all HandlerMapping implementations.

You can combine global CORS configuration at the HandlerMapping level with more fine-grained, handler-level CORS configuration. For example, annotated controllers can use class- or method-level @CrossOrigin annotations (other handlers can implement CorsConfigurationSource).

The rules for combining global and local configuration are generally additive — for example, all global and all local origins. For those attributes where only a single value can be accepted, such as allowCredentials and maxAge, the local overrides the global value. See CorsConfiguration#combine(CorsConfiguration) for more details.

To learn more from the source or to make advanced customizations, see:

  • CorsConfiguration

  • CorsProcessor and DefaultCorsProcessor

  • AbstractHandlerMapping

Credentialed Requests

Using CORS with credentialed requests requires enabling allowedCredentials. Be aware that this option establishes a high level of trust with the configured domains and also increases the surface of attack of the web application by exposing sensitive user-specific information such as cookies and CSRF tokens.

Enabling credentials also impacts how the configured "*" CORS wildcards are processed:

  • Wildcards are not authorized in allowOrigins, but alternatively the allowOriginPatterns property may be used to match to a dynamic set of origins.

  • When set on allowedHeaders or allowedMethods, the Access-Control-Allow-Headers and Access-Control-Allow-Methods response headers are handled by copying the related headers and method specified in the CORS preflight request.

  • When set on exposedHeaders, Access-Control-Expose-Headers response header is set either to the configured list of headers or to the wildcard character. While the CORS spec does not allow the wildcard character when Access-Control-Allow-Credentials is set to true, most browsers support it and the response headers are not all available during the CORS processing, so as a consequence the wildcard character is the header value used when specified regardless of the value of the allowCredentials property.

While such wildcard configuration can be handy, it is recommended when possible to configure a finite set of values instead to provide a higher level of security.

@CrossOrigin

The @CrossOrigin annotation enables cross-origin requests on annotated controller methods, as the following example shows:

  • Java

  • Kotlin

@RestController
@RequestMapping("/account")
public class AccountController {

	@CrossOrigin
	@GetMapping("/{id}")
	public Mono<Account> retrieve(@PathVariable Long id) {
		// ...
	}

	@DeleteMapping("/{id}")
	public Mono<Void> remove(@PathVariable Long id) {
		// ...
	}
}
@RestController
@RequestMapping("/account")
class AccountController {

	@CrossOrigin
	@GetMapping("/{id}")
	suspend fun retrieve(@PathVariable id: Long): Account {
		// ...
	}

	@DeleteMapping("/{id}")
	suspend fun remove(@PathVariable id: Long) {
		// ...
	}
}

By default, @CrossOrigin allows:

  • All origins.

  • All headers.

  • All HTTP methods to which the controller method is mapped.

allowCredentials is not enabled by default, since that establishes a trust level that exposes sensitive user-specific information (such as cookies and CSRF tokens) and should be used only where appropriate. When it is enabled either allowOrigins must be set to one or more specific domain (but not the special value "*") or alternatively the allowOriginPatterns property may be used to match to a dynamic set of origins.

maxAge is set to 30 minutes.

@CrossOrigin is supported at the class level, too, and inherited by all methods. The following example specifies a certain domain and sets maxAge to an hour:

  • Java

  • Kotlin

@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

	@GetMapping("/{id}")
	public Mono<Account> retrieve(@PathVariable Long id) {
		// ...
	}

	@DeleteMapping("/{id}")
	public Mono<Void> remove(@PathVariable Long id) {
		// ...
	}
}
@CrossOrigin("https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

	@GetMapping("/{id}")
	suspend fun retrieve(@PathVariable id: Long): Account {
		// ...
	}

	@DeleteMapping("/{id}")
	suspend fun remove(@PathVariable id: Long) {
		// ...
	}
}

You can use @CrossOrigin at both the class and the method level, as the following example shows:

  • Java

  • Kotlin

@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
public class AccountController {

	@CrossOrigin("https://domain2.com") (2)
	@GetMapping("/{id}")
	public Mono<Account> retrieve(@PathVariable Long id) {
		// ...
	}

	@DeleteMapping("/{id}")
	public Mono<Void> remove(@PathVariable Long id) {
		// ...
	}
}
1 Using @CrossOrigin at the class level.
2 Using @CrossOrigin at the method level.
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
class AccountController {

	@CrossOrigin("https://domain2.com") (2)
	@GetMapping("/{id}")
	suspend fun retrieve(@PathVariable id: Long): Account {
		// ...
	}

	@DeleteMapping("/{id}")
	suspend fun remove(@PathVariable id: Long) {
		// ...
	}
}
1 Using @CrossOrigin at the class level.
2 Using @CrossOrigin at the method level.

Global Configuration

In addition to fine-grained, controller method-level configuration, you probably want to define some global CORS configuration, too. You can set URL-based CorsConfiguration mappings individually on any HandlerMapping. Most applications, however, use the WebFlux Java configuration to do that.

By default global configuration enables the following:

  • All origins.

  • All headers.

  • GET, HEAD, and POST methods.

allowedCredentials is not enabled by default, since that establishes a trust level that exposes sensitive user-specific information (such as cookies and CSRF tokens) and should be used only where appropriate. When it is enabled either allowOrigins must be set to one or more specific domain (but not the special value "*") or alternatively the allowOriginPatterns property may be used to match to a dynamic set of origins.

maxAge is set to 30 minutes.

To enable CORS in the WebFlux Java configuration, you can use the CorsRegistry callback, as the following example shows:

  • Java

  • Kotlin

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

	@Override
	public void addCorsMappings(CorsRegistry registry) {

		registry.addMapping("/api/**")
			.allowedOrigins("https://domain2.com")
			.allowedMethods("PUT", "DELETE")
			.allowedHeaders("header1", "header2", "header3")
			.exposedHeaders("header1", "header2")
			.allowCredentials(true).maxAge(3600);

		// Add more mappings...
	}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

	override fun addCorsMappings(registry: CorsRegistry) {

		registry.addMapping("/api/**")
				.allowedOrigins("https://domain2.com")
				.allowedMethods("PUT", "DELETE")
				.allowedHeaders("header1", "header2", "header3")
				.exposedHeaders("header1", "header2")
				.allowCredentials(true).maxAge(3600)

		// Add more mappings...
	}
}

CORS WebFilter

You can apply CORS support through the built-in CorsWebFilter, which is a good fit with functional endpoints.

If you try to use the CorsFilter with Spring Security, keep in mind that Spring Security has built-in support for CORS.

To configure the filter, you can declare a CorsWebFilter bean and pass a CorsConfigurationSource to its constructor, as the following example shows:

  • Java

  • Kotlin

@Bean
CorsWebFilter corsFilter() {

	CorsConfiguration config = new CorsConfiguration();

	// Possibly...
	// config.applyPermitDefaultValues()

	config.setAllowCredentials(true);
	config.addAllowedOrigin("https://domain1.com");
	config.addAllowedHeader("*");
	config.addAllowedMethod("*");

	UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
	source.registerCorsConfiguration("/**", config);

	return new CorsWebFilter(source);
}
@Bean
fun corsFilter(): CorsWebFilter {

	val config = CorsConfiguration()

	// Possibly...
	// config.applyPermitDefaultValues()

	config.allowCredentials = true
	config.addAllowedOrigin("https://domain1.com")
	config.addAllowedHeader("*")
	config.addAllowedMethod("*")

	val source = UrlBasedCorsConfigurationSource().apply {
		registerCorsConfiguration("/**", config)
	}
	return CorsWebFilter(source)
}