Gymterview
junior

Как работает CORS и как его настроить?

CORS (Cross-Origin Resource Sharing) — механизм, позволяющий браузеру делать запросы к серверу на другом домене (origin). По умолчанию браузер блокирует такие запросы из соображений безопасности (Same-Origin Policy).

Origin (источник)

Origin состоит из трёх частей: протокол + домен + порт.

Пример
https://app.mybank.com:443  — один origin
https://api.mybank.com:443  — другой origin (другой домен)
http://app.mybank.com:80    — другой origin (другой протокол и порт)

Простые и preflight-запросы

Простой запрос (не требует preflight):

  • Метод: GET, HEAD, POST
  • Заголовки: только стандартные (Accept, Content-Type с типом text/plain, multipart/form-data, application/x-www-form-urlencoded)

Preflight-запрос (OPTIONS) выполняется перед основным запросом, если:

  • Метод: PUT, DELETE, PATCH
  • Нестандартные заголовки (например, Authorization)
  • Content-Type: application/json
Схема preflight-запроса
Браузер                                            Сервер
   │──── OPTIONS /api/transfer ──────────────────────>│
   │     Origin: https://app.mybank.com               │
   │     Access-Control-Request-Method: POST           │
   │     Access-Control-Request-Headers: Authorization │
   │<─── 200 OK ─────────────────────────────────────│
   │     Access-Control-Allow-Origin: https://app.mybank.com
   │     Access-Control-Allow-Methods: GET,POST,PUT   │
   │     Access-Control-Max-Age: 3600                  │
   │──── POST /api/transfer ─────────────────────────>│
   │     Authorization: Bearer eyJhbG...              │
   │<─── 200 OK ─────────────────────────────────────│

Настройка CORS в Spring Boot

Пример конфигурации через WebMvcConfigurer
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins(
                "https://app.mybank.com",
                "https://admin.mybank.com"
            )
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("Authorization", "Content-Type", "X-Request-Id")
            .exposedHeaders("X-Total-Count")
            .allowCredentials(true)
            .maxAge(3600);
    }
}
Пример конфигурации через Spring Security
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors(cors -> cors.configurationSource(request -> {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://app.mybank.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        config.setAllowCredentials(true);
        return config;
    }));
    return http.build();
}

Настройка CORS в Nginx

Конфигурация Nginx
location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://app.mybank.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
        add_header 'Access-Control-Max-Age' 3600;
        return 204;
    }

    add_header 'Access-Control-Allow-Origin' 'https://app.mybank.com' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;

    proxy_pass http://backend;
}

Важные правила безопасности

  • Никогда не используйте * в Access-Control-Allow-Origin для критичных приложений — указывайте конкретные домены
  • allowCredentials(true) несовместим с allowedOrigins("*") — это требование спецификации
  • Preflight-ответ кешируется на стороне браузера в течение Access-Control-Max-Age секунд

На собеседовании: интервьюер проверяет понимание Same-Origin Policy, разницу между простым и preflight-запросом и умение настроить CORS в Spring. Частая ошибка — поставить * в allowed origins «чтобы работало» и не понимать, почему preflight падает.