CORS в Spring Webflux
В предыдущих двух статьях я постарался вкратце объяснить, что такое протокол CORS и как он работает, а также как сконфигурировать его поддержку в веб-приложениях на основе Spring и Servlet API. В этой статье я предлагаю разобраться с настройками CORS на стороне реактивного веб-приложения на основе Spring Webflux.
Настройка CORS в Spring Webflux
За поддержку CORS на стороне реактивных веб-приложений, основанных на Spring Webflux, отвечает фильтр CorsWebFilter. Аналогично веб-приложениям на основе Servlet API все необходимые компоненты находятся именно в самом Spring Webflux. Поэтому для включения поддержки CORS в вашем веб-приложении вам не потребуются дополнительные зависимости.
Всё, что нужно для настройки CORS — зарегистрировать в контексте приложения компонент CorsWebFilter:
@Configuration
class WebBeans {
@Bean
CorsWebFilter corsWebFilter() {
// Источник конфигураций CORS
var corsConfigurationSource = new UrlBasedCorsConfigurationSource();
// Конфигурация CORS
var globalCorsConfiguration = new CorsConfiguration();
// Разрешаются CORS-запросы:
// - с сайта http://localhost:8080
globalCorsConfiguration.addAllowedOrigin("http://localhost:8080");
// - с нестандартными заголовками Authorization и X-CUSTOM-HEADER
globalCorsConfiguration.addAllowedHeader(HttpHeaders.AUTHORIZATION);
globalCorsConfiguration.addAllowedHeader("X-CUSTOM-HEADER");
// - с передачей учётных данных
globalCorsConfiguration.setAllowCredentials(true);
// - с методами GET, POST, PUT, PATCH и DELETE
globalCorsConfiguration.setAllowedMethods(List.of(
HttpMethod.GET.name(),
HttpMethod.POST.name(),
HttpMethod.PUT.name(),
HttpMethod.PATCH.name(),
HttpMethod.DELETE.name()
));
// JavaScript может обращаться к заголовку X-OTHER-CUSTOM-HEADER ответа
globalCorsConfiguration.setExposedHeaders(List.of("X-OTHER-CUSTOM-HEADER"));
// Браузер может кешировать настройки CORS на 10 секунд
globalCorsConfiguration.setMaxAge(Duration.ofSeconds(10));
// Использование конфигурации CORS для всех запросов
corsConfigurationSource.registerCorsConfiguration("/**";, globalCorsConfiguration);
return new CorsWebFilter(corsConfigurationSource);
}
}Основные компоненты, используемые для настройки CORS: CorsConfigurationSource и CorsConfiguration.
CorsConfigurationSource
Интерфейс CorsConfigurationSource объявляет метод getCorsConfiguration, который возвращает настройки CORS, наиболее подходящие для полученного запроса.
Основная реализация данного интерфейса — это класс UrlBasedCorsConfigurationSource, который может хранить множество настроек CORS и определяет наиболее подходящие по URL запроса.
Для регистрации настроек CORS в классе UrlBasedCorsConfigurationSource есть метод registerCorsConfiguration, который в качестве аргумента принимает шаблон URL и связанные с ним настройки.
CorsConfiguration
Класс CorsConfiguration описывает настройки CORS и содержит следующие свойства:
allowedOrigins— список разрешённых адресов, с которых могут выполняться CORS-запросыallowedMethods— список разрешённых методовallowedHeaders— список разрешённых заголовковexposedHeaders— список заголовков, к которым может иметь доступ JavaScriptallowCredentials— разрешено ли передавать учётные данные в запросахallowPrivateNetwork— разрешены ли запросы из-за пределов частной сетиmaxAge— максимальное время в секундах, которое браузер должен хранить настройки CORS
Настройка CORS в Spring Security Reactive
В Spring Security Reactive для CORS есть соответствующий конфигуратор, позволяющий сконфигурировать CORS привычным для Spring Security способом:
@Configuration
class SecurityBeans {
@Bean
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.cors(configurer -> {
// Источник конфигураций CORS
var corsConfigurationSource = new UrlBasedCorsConfigurationSource();
// Конфигурация CORS
var globalCorsConfiguration = new CorsConfiguration();
// Разрешаются CORS-запросы:
// - с сайта http://localhost:8080
globalCorsConfiguration.addAllowedOrigin("http://localhost:8080");
// - с нестандартными заголовками Authorization и X-CUSTOM-HEADER
globalCorsConfiguration.addAllowedHeader(HttpHeaders.AUTHORIZATION);
globalCorsConfiguration.addAllowedHeader("X-CUSTOM-HEADER");
// - с передачей учётных данных
globalCorsConfiguration.setAllowCredentials(true);
// - с методами GET, POST, PUT, PATCH и DELETE
globalCorsConfiguration.setAllowedMethods(List.of(
HttpMethod.GET.name(),
HttpMethod.POST.name(),
HttpMethod.PUT.name(),
HttpMethod.PATCH.name(),
HttpMethod.DELETE.name()
));
// JavaScript может обращаться к заголовку X-OTHER-CUSTOM-HEADER ответа
globalCorsConfiguration.setExposedHeaders(List.of("X-OTHER-CUSTOM-HEADER"));
// Браузер может кешировать настройки CORS на 10 секунд
globalCorsConfiguration.setMaxAge(Duration.ofSeconds(10));
// Использование конфигурации CORS для всех запросов
corsConfigurationSource.registerCorsConfiguration("/**", globalCorsConfiguration);
configurer.configurationSource(corsConfigurationSource);
})
.build();
}
}Если в контексте приложения зарегистрирован компоненты CorsWebFilter или CorsConfigurationSource, то Spring Security будет использовать их.
Оптимизация работы со Spring Boot
Spring Boot не предоставляет инструментов для быстрой настройки CORS, и вероятно не будет. Однако в реальных условиях хочется иметь возможность динамически гибко изменять настройки CORS без необходимости в последующей пересборке проекта. Для этого мне потребуется класс свойств, который будет содержать связку между шаблоном пути и параметрами CORS. Я могу также создать класс, описывающий параметры CORS, но вместо этого я буду использовать существующий CorsProperties:
@ConfigurationProperties(prefix = "spring.cors.url")
public class UrlBasedCorsConfigurationProperties {
private Map<String, CorsConfiguration> configurations = new LinkedHashMap<>();
public Map<String, CorsConfiguration> getConfigurations() {
return this.configurations;
}
public void setConfigurations(LinkedHashMap<String, CorsConfiguration> configurations) {
this.configurations = configurations;
}
}Для реализации используется именно LinkedHashMap, так как желательно применять правила в том порядке, в котором они описываются.
Теперь мне нужно зарегистрировать данный класс свойств в приложении:
@SpringBootApplication
@EnableConfigurationProperties(UrlBasedCorsConfigurationProperties.class)
public class CorsSandboxApplication {
public static void main(String[] args) {
SpringApplication.run(CorsSandboxApplication.class, args);
}
}Теперь я могу описать настройки CORS в application.yaml следующим образом:
spring:
cors:
url:
configurations:
# Настройки CORS для путей, начинающихся с /api
"[/api/**]":
allowed-origins:
- http://localhost:8081
allowed-methods:
- GET
- POST
- PATCH
- DELETE
allowed-headers:
- Authorization
- Content-Type
max-age: 10
allow-credentials: true
# Для всех запросов разрешаются CORS-запросы:
"[/**]":
# - с сайта http://localhost:8080
allowed-origins:
- http://localhost:8080
# - с методами GET и POST
allowed-methods:
- GET
- POST
# - с нестандартными заголовками Authorization и Content-Type
allowed-headers:
- Authorization
- Content-Type
# - с передачей учётных данных
allow-credentials: true
# JavaScript может обращаться к заголовку X-CUSTOM-HEADER ответа
exposed-headers:
- X-CUSTOM-HEADER
# Браузер может кешировать настройки CORS на 10 секунд
max-age: 10Обратите ваше внимание на экранирование шаблонов путей: "[/**]", нужно это чтобы не терялся ведущий / и не возникало ошибок парсинга.
Дело осталось за малым — использовать UrlBasedCorsConfigurationProperties для создания CorsWebFilter. Если не используется Spring Security, это будет выглядеть следующим образом:
@Configuration
class WebBeans {
@Bean
CorsWebFilter corsWebFilter(UrlBasedCorsConfigurationProperties properties) {
// Источник конфигураций CORS
var source = new UrlBasedCorsConfigurationSource();
properties.getConfigurations().forEach(source::registerCorsConfiguration);
return new CorsWebFilter(source);
}
}И если используется Spring Security:
@Configuration
class SecurityBeans {
@Bean
SecurityWebFilterChain securityFilterChain(HttpServerSecurity http,
UrlBasedCorsConfigurationProperties properties) {
return http
.cors(configurer -> {
// Источник конфигураций CORS
var source = new UrlBasedCorsConfigurationSource();
properties.getConfigurations().forEach(source::registerCorsConfiguration);
configurer.configurationSource(source);
})
.build();
}
}