mirror of
https://github.com/spring-projects/spring-security.git
synced 2026-03-30 22:12:48 +00:00
Add Support for PreFlightRequestFilter
Closes gh-18926
This commit is contained in:
parent
0ef8a4ff27
commit
4199240662
@ -21,15 +21,18 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.PreFlightRequestHandler;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import org.springframework.web.filter.PreFlightRequestFilter;
|
||||
|
||||
/**
|
||||
* Adds {@link CorsFilter} to the Spring Security filter chain. If a bean by the name of
|
||||
* corsFilter is provided, that {@link CorsFilter} is used. Else if
|
||||
* corsConfigurationSource is defined, then that {@link CorsConfiguration} is used.
|
||||
* Adds {@link CorsFilter} or {@link PreFlightRequestFilter} to the Spring Security filter
|
||||
* chain. If a bean by the name of corsFilter is provided, that {@link CorsFilter} is
|
||||
* used. Else if corsConfigurationSource is defined, then that
|
||||
* {@link CorsConfigurationSource} is used. If a {@link PreFlightRequestHandler} is set on
|
||||
* this configurer, {@link CorsFilter} is not used and {@link PreFlightRequestFilter} is
|
||||
* registered instead.
|
||||
*
|
||||
* @param <H> the builder to return.
|
||||
* @author Rob Winch
|
||||
@ -43,6 +46,8 @@ public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHt
|
||||
|
||||
private CorsConfigurationSource configurationSource;
|
||||
|
||||
private PreFlightRequestHandler preFlightRequestHandler;
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
*
|
||||
@ -56,30 +61,85 @@ public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHt
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the given {@link PreFlightRequestHandler} for CORS preflight requests. When
|
||||
* set, {@link CorsFilter} is not used. Cannot be combined with
|
||||
* {@link #configurationSource(CorsConfigurationSource)}.
|
||||
* @param preFlightRequestHandler the handler to use
|
||||
* @return the {@link CorsConfigurer} for additional configuration
|
||||
*/
|
||||
public CorsConfigurer<H> preFlightRequestHandler(PreFlightRequestHandler preFlightRequestHandler) {
|
||||
this.preFlightRequestHandler = preFlightRequestHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(H http) {
|
||||
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
|
||||
|
||||
if (this.configurationSource != null && this.preFlightRequestHandler != null) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot configure both a CorsConfigurationSource and a PreFlightRequestHandler on CorsConfigurer");
|
||||
}
|
||||
|
||||
CorsFilter corsFilter = getCorsFilter(context);
|
||||
Assert.state(corsFilter != null, () -> "Please configure either a " + CORS_FILTER_BEAN_NAME + " bean or a "
|
||||
+ CORS_CONFIGURATION_SOURCE_BEAN_NAME + "bean.");
|
||||
http.addFilter(corsFilter);
|
||||
if (corsFilter != null) {
|
||||
http.addFilter(corsFilter);
|
||||
return;
|
||||
}
|
||||
PreFlightRequestHandler preFlightRequestHandlerBean = getPreFlightRequestHandler(context);
|
||||
if (preFlightRequestHandlerBean != null) {
|
||||
http.addFilterBefore(new PreFlightRequestFilter(preFlightRequestHandlerBean), CorsFilter.class);
|
||||
return;
|
||||
}
|
||||
throw new NoSuchBeanDefinitionException(CorsConfigurationSource.class,
|
||||
"Failed to find a bean that implements `CorsConfigurationSource`. Please ensure that you are using "
|
||||
+ "`@EnableWebMvc`, are publishing a `WebMvcConfigurer`, or are publishing a `CorsConfigurationSource` bean.");
|
||||
}
|
||||
|
||||
private PreFlightRequestHandler getPreFlightRequestHandler(ApplicationContext context) {
|
||||
if (this.configurationSource != null) {
|
||||
return null;
|
||||
}
|
||||
if (this.preFlightRequestHandler != null) {
|
||||
return this.preFlightRequestHandler;
|
||||
}
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
if (context.getBeanNamesForType(PreFlightRequestHandler.class).length > 0) {
|
||||
return context.getBean(PreFlightRequestHandler.class);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private CorsConfigurationSource getCorsConfigurationSource(ApplicationContext context) {
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
boolean containsCorsSource = context.containsBeanDefinition(CORS_CONFIGURATION_SOURCE_BEAN_NAME);
|
||||
if (containsCorsSource) {
|
||||
return context.getBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME, CorsConfigurationSource.class);
|
||||
}
|
||||
return MvcCorsFilter.getMvcCorsConfigurationSource(context);
|
||||
}
|
||||
|
||||
private CorsFilter getCorsFilter(ApplicationContext context) {
|
||||
if (this.preFlightRequestHandler != null) {
|
||||
return null;
|
||||
}
|
||||
if (this.configurationSource != null) {
|
||||
return new CorsFilter(this.configurationSource);
|
||||
}
|
||||
boolean containsCorsFilter = context.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
|
||||
boolean containsCorsFilter = context != null && context.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
|
||||
if (containsCorsFilter) {
|
||||
return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class);
|
||||
}
|
||||
boolean containsCorsSource = context.containsBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME);
|
||||
if (containsCorsSource) {
|
||||
CorsConfigurationSource configurationSource = context.getBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME,
|
||||
CorsConfigurationSource.class);
|
||||
return new CorsFilter(configurationSource);
|
||||
CorsConfigurationSource corsConfigurationSource = getCorsConfigurationSource(context);
|
||||
if (corsConfigurationSource != null) {
|
||||
return new CorsFilter(corsConfigurationSource);
|
||||
}
|
||||
return MvcCorsFilter.getMvcCorsFilter(context);
|
||||
return null;
|
||||
}
|
||||
|
||||
static class MvcCorsFilter {
|
||||
@ -92,15 +152,11 @@ public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHt
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
private static CorsFilter getMvcCorsFilter(ApplicationContext context) {
|
||||
private static CorsConfigurationSource getMvcCorsConfigurationSource(ApplicationContext context) {
|
||||
if (context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
|
||||
CorsConfigurationSource corsConfigurationSource = context
|
||||
.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, CorsConfigurationSource.class);
|
||||
return new CorsFilter(corsConfigurationSource);
|
||||
return context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, CorsConfigurationSource.class);
|
||||
}
|
||||
throw new NoSuchBeanDefinitionException(CorsConfigurationSource.class,
|
||||
"Failed to find a bean that implements `CorsConfigurationSource`. Please ensure that you are using "
|
||||
+ "`@EnableWebMvc`, are publishing a `WebMvcConfigurer`, or are publishing a `CorsConfigurationSource` bean.");
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer
|
||||
import org.springframework.web.cors.CorsConfigurationSource
|
||||
import org.springframework.web.cors.PreFlightRequestHandler
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure [HttpSecurity] CORS using idiomatic Kotlin code.
|
||||
@ -26,11 +27,14 @@ import org.springframework.web.cors.CorsConfigurationSource
|
||||
* @author Eleftheria Stein
|
||||
* @since 5.3
|
||||
* @property configurationSource the [CorsConfigurationSource] to use.
|
||||
* @property preFlightRequestHandler the [PreFlightRequestHandler] to use instead of [CorsFilter].
|
||||
*/
|
||||
@SecurityMarker
|
||||
class CorsDsl {
|
||||
var configurationSource: CorsConfigurationSource? = null
|
||||
|
||||
var preFlightRequestHandler: PreFlightRequestHandler? = null
|
||||
|
||||
private var disabled = false
|
||||
|
||||
/**
|
||||
@ -42,7 +46,8 @@ class CorsDsl {
|
||||
|
||||
internal fun get(): (CorsConfigurer<HttpSecurity>) -> Unit {
|
||||
return { cors ->
|
||||
configurationSource?.also { cors.configurationSource(configurationSource) }
|
||||
configurationSource?.also { cors.configurationSource(it) }
|
||||
preFlightRequestHandler?.also { cors.preFlightRequestHandler(it) }
|
||||
if (disabled) {
|
||||
cors.disable()
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.PreFlightRequestHandler;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
@ -196,6 +197,73 @@ public class CorsConfigurerTests {
|
||||
.andExpect(header().exists("X-Content-Type-Options"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionsWhenPreFlightRequestHandlerBeanThenHandled() throws Exception {
|
||||
this.spring.register(PreFlightRequestHandlerConfig.class).autowire();
|
||||
this.mvc
|
||||
.perform(options("/")
|
||||
.header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
|
||||
.header(HttpHeaders.ORIGIN, "https://example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().exists("X-Pre-Flight"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionsWhenNoPreFlightRequestHandlerBeanThenCorsFilterUsed() throws Exception {
|
||||
this.spring.register(NoPreFlightRequestHandlerConfig.class).autowire();
|
||||
this.mvc
|
||||
.perform(options("/")
|
||||
.header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
|
||||
.header(HttpHeaders.ORIGIN, "https://example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().exists("Access-Control-Allow-Origin"))
|
||||
.andExpect(header().doesNotExist("X-Pre-Flight"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionsWhenExplicitConfigurationSourceThenPreFlightRequestHandlerBeanIgnored() throws Exception {
|
||||
this.spring.register(ExplicitConfigurationSourceWithPreFlightRequestHandlerBeanConfig.class).autowire();
|
||||
this.mvc
|
||||
.perform(options("/")
|
||||
.header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
|
||||
.header(HttpHeaders.ORIGIN, "https://example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().exists("Access-Control-Allow-Origin"))
|
||||
.andExpect(header().doesNotExist("X-Pre-Flight"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionsWhenPreFlightRequestHandlerMemberThenHandled() throws Exception {
|
||||
this.spring.register(PreFlightRequestHandlerMemberConfig.class).autowire();
|
||||
this.mvc
|
||||
.perform(options("/")
|
||||
.header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
|
||||
.header(HttpHeaders.ORIGIN, "https://example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().exists("X-Pre-Flight"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureWhenBothConfigurationSourceAndPreFlightRequestHandlerMemberThenIllegalState() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> this.spring.register(BothCorsConfigurerMembersConfig.class).autowire())
|
||||
.havingRootCause()
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.withMessageContaining("Cannot configure both");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionsWhenPreFlightRequestHandlerMemberThenCorsFilterBeanIgnored() throws Exception {
|
||||
this.spring.register(PreFlightRequestHandlerMemberWithCorsFilterBeanConfig.class).autowire();
|
||||
this.mvc
|
||||
.perform(options("/")
|
||||
.header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name())
|
||||
.header(HttpHeaders.ORIGIN, "https://example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().exists("X-Pre-Flight"))
|
||||
.andExpect(header().doesNotExist("Access-Control-Allow-Origin"));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class DefaultCorsConfig {
|
||||
@ -382,4 +450,150 @@ public class CorsConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class PreFlightRequestHandlerConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().authenticated())
|
||||
.cors(withDefaults());
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
PreFlightRequestHandler preFlightRequestHandler() {
|
||||
return (request, response) -> response.addHeader("X-Pre-Flight", "Handled");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class NoPreFlightRequestHandlerConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().authenticated())
|
||||
.cors(withDefaults());
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
CorsConfigurationSource corsConfigurationSource() {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
|
||||
corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name()));
|
||||
source.registerCorsConfiguration("/**", corsConfiguration);
|
||||
return source;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class ExplicitConfigurationSourceWithPreFlightRequestHandlerBeanConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
|
||||
corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name()));
|
||||
source.registerCorsConfiguration("/**", corsConfiguration);
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().authenticated())
|
||||
.cors((cors) -> cors.configurationSource(source));
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
PreFlightRequestHandler preFlightRequestHandler() {
|
||||
return (request, response) -> response.addHeader("X-Pre-Flight", "Handled");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class PreFlightRequestHandlerMemberConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().authenticated())
|
||||
.cors((cors) -> cors.preFlightRequestHandler(
|
||||
(request, response) -> response.addHeader("X-Pre-Flight", "Member")));
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class BothCorsConfigurerMembersConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
|
||||
corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name()));
|
||||
source.registerCorsConfiguration("/**", corsConfiguration);
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().authenticated())
|
||||
.cors((cors) -> cors
|
||||
.configurationSource(source)
|
||||
.preFlightRequestHandler((request, response) -> response.addHeader("X-Pre-Flight", "Handled")));
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class PreFlightRequestHandlerMemberWithCorsFilterBeanConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests
|
||||
.anyRequest().authenticated())
|
||||
.cors((cors) -> cors.preFlightRequestHandler(
|
||||
(request, response) -> response.addHeader("X-Pre-Flight", "Member")));
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
CorsFilter corsFilter() {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
|
||||
corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name()));
|
||||
source.registerCorsConfiguration("/**", corsConfiguration);
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -16,7 +16,10 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.assertj.core.api.Assertions.catchThrowable
|
||||
import org.assertj.core.util.Throwables
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.BeanCreationException
|
||||
@ -35,9 +38,14 @@ import org.springframework.test.web.servlet.get
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMethod
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.CorsConfigurationSource
|
||||
import org.springframework.web.cors.PreFlightRequestHandler
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||
import org.springframework.web.filter.CorsFilter
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc
|
||||
|
||||
/**
|
||||
@ -153,7 +161,7 @@ class CorsDslTests {
|
||||
|
||||
@Test
|
||||
fun `CORS when CORS configuration source dsl then responds with CORS header`() {
|
||||
this.spring.register(CorsCrossOriginBeanConfig::class.java, HomeController::class.java).autowire()
|
||||
this.spring.register(CorsCrossOriginSourceConfig::class.java, HomeController::class.java).autowire()
|
||||
|
||||
this.mockMvc.get("/")
|
||||
{
|
||||
@ -185,6 +193,117 @@ class CorsDslTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CORS when preFlight request handler dsl then OPTIONS uses handler`() {
|
||||
this.spring.register(PreFlightRequestHandlerDslConfig::class.java).autowire()
|
||||
|
||||
this.mockMvc.perform(options("/")
|
||||
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, RequestMethod.POST.name)
|
||||
.header(HttpHeaders.ORIGIN, "https://example.com"))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(header().exists("X-Pre-Flight"))
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class PreFlightRequestHandlerDslConfig {
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
cors {
|
||||
preFlightRequestHandler = PreFlightRequestHandler { _, response ->
|
||||
response.addHeader("X-Pre-Flight", "Dsl")
|
||||
}
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CORS when configuration source and preFlight handler dsl then illegal state`() {
|
||||
val thrown = catchThrowable {
|
||||
this.spring.register(BothCorsDslMembersConfig::class.java).autowire()
|
||||
}
|
||||
assertThat(thrown).isInstanceOf(BeanCreationException::class.java)
|
||||
assertThat(Throwables.getRootCause(thrown))
|
||||
.isInstanceOf(IllegalStateException::class.java)
|
||||
.hasMessageContaining("Cannot configure both")
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class BothCorsDslMembersConfig {
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
val source = UrlBasedCorsConfigurationSource()
|
||||
val corsConfiguration = CorsConfiguration()
|
||||
corsConfiguration.allowedOrigins = listOf("*")
|
||||
corsConfiguration.allowedMethods = listOf(
|
||||
RequestMethod.GET.name,
|
||||
RequestMethod.POST.name)
|
||||
source.registerCorsConfiguration("/**", corsConfiguration)
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
cors {
|
||||
configurationSource = source
|
||||
preFlightRequestHandler = PreFlightRequestHandler { _, response ->
|
||||
response.addHeader("X-Pre-Flight", "Dsl")
|
||||
}
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CORS when preFlight handler dsl then CorsFilter bean ignored on OPTIONS`() {
|
||||
this.spring.register(PreFlightRequestHandlerDslWithCorsFilterBeanConfig::class.java).autowire()
|
||||
|
||||
this.mockMvc.perform(options("/")
|
||||
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, RequestMethod.POST.name)
|
||||
.header(HttpHeaders.ORIGIN, "https://example.com"))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(header().exists("X-Pre-Flight"))
|
||||
.andExpect(header().doesNotExist("Access-Control-Allow-Origin"))
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class PreFlightRequestHandlerDslWithCorsFilterBeanConfig {
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
cors {
|
||||
preFlightRequestHandler = PreFlightRequestHandler { _, response ->
|
||||
response.addHeader("X-Pre-Flight", "Dsl")
|
||||
}
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun corsFilter(): CorsFilter {
|
||||
val source = UrlBasedCorsConfigurationSource()
|
||||
val corsConfiguration = CorsConfiguration()
|
||||
corsConfiguration.allowedOrigins = listOf("*")
|
||||
corsConfiguration.allowedMethods = listOf(
|
||||
RequestMethod.GET.name,
|
||||
RequestMethod.POST.name)
|
||||
source.registerCorsConfiguration("/**", corsConfiguration)
|
||||
return CorsFilter(source)
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
private class HomeController {
|
||||
@GetMapping("/")
|
||||
|
||||
@ -184,6 +184,23 @@ fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {
|
||||
----
|
||||
======
|
||||
|
||||
[[cors-preflight-request-handler]]
|
||||
== `PreFlightRequestHandler` and `PreFlightRequestFilter`
|
||||
|
||||
Spring Framework defines {spring-framework-api-url}org/springframework/web/cors/PreFlightRequestHandler.html[`PreFlightRequestHandler`] for applications that need to handle CORS preflight (`OPTIONS`) requests outside of `CorsFilter`.
|
||||
When Spring Security selects a `PreFlightRequestHandler` for a filter chain, it registers {spring-framework-api-url}org/springframework/web/filter/PreFlightRequestFilter.html[`PreFlightRequestFilter`] in the security filter chain (before `CorsFilter`) so preflight can be handled early in the request lifecycle.
|
||||
|
||||
You can supply a handler in either of these ways:
|
||||
|
||||
* Pass a handler directly with the `preFlightRequestHandler` attribute.
|
||||
* Register a `PreFlightRequestHandler` bean when cors is enabled and when no `CorsConfigurationSource` or `CorsFilter` is chosen for that chain.
|
||||
|
||||
You must not configure both `configurationSource` and `preFlightRequestHandler` on the same `CorsConfigurer`; doing so results in an error at startup.
|
||||
|
||||
The following example explicitly registers a `PreFlightRequestHandler` using the `preFlightRequestHandler`:
|
||||
|
||||
include-code::./CorsPreFlightRequestHandlerExample[tag=preflightRequestHandler,indent=0]
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
CORS is a browser-based security feature.
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
* Added xref:servlet/authorization/architecture.adoc#authz-conditional-authorization-manager[ConditionalAuthorizationManager]
|
||||
* Added `when` and `withWhen` conditions to `AuthorizationManagerFactories.multiFactor()` for xref:servlet/authentication/mfa.adoc#programmatic-mfa[Programmatic MFA]
|
||||
* Added `MultiFactorCondition.WEBAUTHN_REGISTERED` to `@EnableMultiFactorAuthentication(when = ...)` for xref:servlet/authentication/mfa.adoc#mfa-when-webauthn-registered[conditionally requiring MFA for WebAuthn Users]
|
||||
* https://github.com/spring-projects/spring-security/issues/18926[gh-18926] - xref:servlet/integrations/cors.adoc[Add `PreFlightRequestFilter` Support]
|
||||
|
||||
== OAuth 2.0
|
||||
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2004-present 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.security.docs.servlet.integrations.corspreflightrequesthandler;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.PreFlightRequestHandler;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class CorsPreFlightRequestHandlerExample {
|
||||
|
||||
@Bean
|
||||
PreFlightRequestHandler preFlightRequestHandler() {
|
||||
return (request, response) -> {
|
||||
// custom preflight handling (for example, write CORS headers or complete the response)
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain springSecurity(HttpSecurity http, PreFlightRequestHandler preFlightRequestHandler) {
|
||||
// tag::preflightRequestHandler[]
|
||||
http
|
||||
// ..
|
||||
.cors((cors) -> cors
|
||||
.preFlightRequestHandler(preFlightRequestHandler)
|
||||
);
|
||||
return http.build();
|
||||
// end::preflightRequestHandler[]
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2004-present 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.security.kt.docs.servlet.integrations.corspreflightrequesthandler
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.web.cors.PreFlightRequestHandler
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class CorsPreFlightRequestHandlerExample {
|
||||
|
||||
@Bean
|
||||
fun preFlightRequestHandler(): PreFlightRequestHandler {
|
||||
return PreFlightRequestHandler { _, _ ->
|
||||
// custom preflight handling (for example, write CORS headers or complete the response)
|
||||
}
|
||||
}
|
||||
|
||||
// tag::preflightRequestHandler[]
|
||||
@Bean
|
||||
fun springSecurity(http: HttpSecurity, preFlightRequestHandler: PreFlightRequestHandler): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
cors {
|
||||
this.preFlightRequestHandler = preFlightRequestHandler
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
// end::preflightRequestHandler[]
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user