CORS в Spring Web
Alexander KosarevВ предыдущей статье я постарался вкратце объяснить, что такое протокол CORS и как он работает. В этой статье я предлагаю разобраться с настройками CORS на стороне Spring Web, Spring Security и Spring Boot.
Настройка CORS в Spring Web
За поддержку CORS на стороне веб-приложений, основанных на Servlet API, в экосистеме Spring отвечает именно Spring Web — фильтр CorsFilter (не путать с CorsFilter из Catalina! и все необходимые для его настройки классы и интерфейсы находятся именно в этом модуле, а не в Spring Security. Поэтому для включения поддержки CORS в вашем веб-приложении на основе Spring вам не потребуются дополнительные зависимости.
Всё, что нужно для настройки CORS — зарегистрировать в контексте приложения компонент CorsFilter:
@Configuration
class WebBeans {
@Bean
CorsFilter corsFilter() {
// Источник конфигураций 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 CorsFilter(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
В Spring Security для CORS есть соответствующий конфигуратор, позволяющий сконфигурировать CORS привычным для Spring Security способом:
@Configuration
class SecurityBeans {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
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();
}
}
Если в контексте приложения зарегистрирован компоненты CorsFilter или 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 для создания CorsFilter. Если не используется Spring Security, это будет выглядеть следующим образом:
@Configuration
class WebBeans {
@Bean
CorsFilter corsFilter(UrlBasedCorsConfigurationProperties properties) {
// Источник конфигураций CORS
var source = new UrlBasedCorsConfigurationSource();
properties.getConfigurations()
.forEach(source::registerCorsConfiguration);
return new CorsFilter(source);
}
}
И если используется Spring Security:
@Configuration
class SecurityBeans {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http,
UrlBasedCorsConfigurationProperties properties) throws Exception {
return http
.cors(configurer -> {
// Источник конфигураций CORS
var source = new UrlBasedCorsConfigurationSource();
properties.getConfigurations()
.forEach(source::registerCorsConfiguration);
configurer.configurationSource(source);
})
.build();
}
}