Spring-Security

CORS (Cross-Origin Resource Sharing): 개념, 작동 방식, Spring Security에서의 처리

neal89 2025. 5. 29. 13:18

1. CORS란 무엇이며, 왜 나왔는가?

**CORS (Cross-Origin Resource Sharing)**는 웹 페이지의 제한된 리소스가 다른 도메인으로부터 요청될 수 있도록 허용하는 메커니즘입니다. 즉, 웹 브라우저가 다른 출처(Origin)의 리소스에 접근할 수 있도록 허용하는 표준 방식입니다.

왜 나왔는가? (Same-Origin Policy의 등장과 한계)

웹 보안의 근간에는 Same-Origin Policy (동일 출원 정책)가 있습니다. 이 정책은 한 출처에서 로드된 웹 페이지가 다른 출처의 리소스와 상호작용하는 것을 제한하여 악성 웹사이트가 사용자의 데이터를 훔치거나 제어하는 것을 방지합니다.

문제는 웹 서비스가 발전하면서 프론트엔드(예: React, Vue)와 백엔드 API 서버(예: Spring Boot)가 다른 도메인에 배포되거나, 여러 외부 API를 활용해야 하는 경우가 빈번해졌다는 것입니다. 이처럼 다른 출처 간에 리소스 공유가 필요해지면서 Same-Origin Policy는 강력한 보안 메커니즘인 동시에 Cross-Origin 통신을 방해하는 제약이 되었습니다.

이를 해결하기 위해 서버가 명시적으로 허용하는 Cross-Origin 요청에 한해 리소스 접근을 허용하는 표준 방식인 CORS가 등장했습니다.

2. CORS 요청의 종류: Simple Request vs. Preflight Request

CORS 요청은 크게 두 가지 유형으로 나뉩니다.

1) Simple Request (단순 요청)

특정 조건을 만족하는 요청은 Preflight Request 없이 바로 서버에 전송됩니다. 브라우저는 서버 응답의 CORS 헤더를 확인하여 접근 허용 여부를 판단합니다.

조건:

  • HTTP 메서드: GET, HEAD, POST 중 하나여야 합니다.
  • 허용된 Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야 합니다.
  • 커스텀 헤더 없음: Accept, Accept-Language, Content-Language, Content-Type (위 허용된 값), Last-Event-ID 등 CORS 안전 헤더 외의 다른 커스텀 헤더를 사용하지 않아야 합니다.

작동 방식:

  1. 클라이언트 (브라우저): CORS 헤더(Origin 등)를 포함하여 요청을 서버로 직접 전송합니다.
  2. 서버: 요청을 처리하고, Access-Control-Allow-Origin 등 CORS 응답 헤더를 포함하여 응답을 보냅니다.
  3. 클라이언트 (브라우저): 서버 응답의 CORS 헤더를 확인합니다. Origin이 허용되면 응답을 애플리케이션으로 전달하고, 허용되지 않으면 에러를 발생시킵니다.

2) Preflight Request (사전 요청)

Simple Request의 조건을 만족하지 않는 복잡한 요청(예: PUT, DELETE 메서드 사용, 커스텀 헤더 포함, application/json Content-Type 사용 등)은 실제 요청을 보내기 전에 브라우저가 서버에게 "이러한 요청을 보내도 괜찮을까요?"라고 묻는 OPTIONS 메서드의 사전 요청을 먼저 보냅니다.

작동 방식:

  1. 클라이언트 (브라우저): 실제 요청을 보내기 전에 OPTIONS 메서드를 사용하여 Preflight Request를 서버로 전송합니다. 이 요청에는 실제 요청에 포함될 HTTP 메서드(Access-Control-Request-Method)와 헤더(Access-Control-Request-Headers) 정보가 포함됩니다.
  2. 서버: Preflight Request를 수신하고, 실제 요청이 허용 가능한지 여부를 판단합니다. 허용 가능한 경우, Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age (사전 요청 결과를 캐싱할 시간) 등의 CORS 응답 헤더를 포함하여 응답을 보냅니다. 이 응답에는 실제 요청의 데이터는 포함되지 않습니다.
  3. 클라이언트 (브라우저): Preflight Request에 대한 서버의 응답을 확인합니다.
    • 허용되면: 실제 요청(본 요청)을 다시 서버로 전송합니다.
    • 허용되지 않으면: 실제 요청을 보내지 않고 CORS 에러를 발생시킵니다.
  4. 서버 (두 번째 요청): 실제 요청을 처리하고 응답을 반환합니다.

Preflight RequestCross-Origin 통신 시 보안을 한 번 더 강화하는 역할을 합니다. 서버가 허용하지 않는 요청이 실수로 데이터를 변경하는 등의 부작용을 사전에 방지할 수 있습니다.

3. Spring Security에서 CORS 처리 방법

Spring Security는 웹 보안 프레임워크이므로 CORS 처리와 밀접하게 관련되어 있습니다. Spring Security 5.x 버전부터는 CORS를 직접 지원하며, WebSecurityConfigurerAdapter (최신 버전에서는 SecurityFilterChain 빈 구성)를 통해 설정을 간편하게 할 수 있습니다.

Spring Security가 CORS를 처리하는 주요 방법은 다음과 같습니다.

  • CorsFilter: Spring Security 필터 체인에 CorsFilter를 추가하여 CORS 요청을 처리합니다. 이 필터는 Preflight RequestSimple Request 모두를 가로채어 적절한 CORS 헤더를 추가하거나 요청을 거부하는 역할을 합니다. CorsFilter는 일반적으로 Spring Security 필터 체인의 초기에 위치하여 모든 요청에 대해 CORS 검사를 수행합니다.
  • CorsConfigurationSource: CorsFilterCorsConfigurationSource를 사용하여 어떤 출처, 메서드, 헤더 등을 허용할지 결정합니다. 개발자는 이 소스를 통해 세부적인 CORS 정책을 정의할 수 있습니다.

예시 (Spring Security 6.x 이상, Java Config):

Java
 
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.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 활성화 및 설정 소스 지정
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().permitAll() // 예시로 모든 요청 허용
            )
            .csrf(csrf -> csrf.disable()); // CSRF 비활성화 (CORS와는 별개)
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://another.domain.com")); // 허용할 Origin 설정
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 허용할 HTTP 메서드 설정
        configuration.setAllowedHeaders(Arrays.asList("*")); // 모든 헤더 허용
        configuration.setAllowCredentials(true); // 자격 증명(쿠키, HTTP 인증) 허용
        configuration.setMaxAge(3600L); // Preflight 요청 결과 캐싱 시간 (초)

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration); // 모든 경로에 대해 CORS 설정 적용
        return source;
    }
}

4. CORS 처리를 위한 API (Spring Framework 기준)

Spring Framework는 CORS를 처리하기 위한 다양한 API와 방법을 제공합니다.

  1. Global CORS Configuration (글로벌 설정):
    • 애플리케이션 전체에 적용되는 CORS 정책을 정의하는 가장 일반적인 방법입니다.
    • API: WebMvcConfigurer 인터페이스의 addCorsMappings() 메서드를 오버라이드하여 설정합니다.
    • 예시:
      Java
       
      import org.springframework.context.annotation.Configuration;
      import org.springframework.web.servlet.config.annotation.CorsRegistry;
      import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
      
      @Configuration
      public class WebConfig implements WebMvcConfigurer {
          @Override
          public void addCorsMappings(CorsRegistry registry) {
              registry.addMapping("/**") // 모든 경로에 대해
                      .allowedOrigins("http://localhost:3000", "http://another.domain.com") // 허용할 Origin
                      .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드
                      .allowedHeaders("*") // 모든 헤더 허용
                      .allowCredentials(true) // 자격 증명 허용
                      .maxAge(3600); // Preflight 요청 캐싱 시간
          }
      }
      
    • 장점: 중앙 집중식으로 관리되어 편리합니다.
  2. Controller / Method Level CORS Configuration (@CrossOrigin):
    • 특정 컨트롤러 클래스나 개별 핸들러 메서드에만 CORS 정책을 적용할 때 사용합니다.
    • API: @CrossOrigin 어노테이션을 사용합니다.
    • 예시:
      Java
       
      import org.springframework.web.bind.annotation.CrossOrigin;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      @RestController
      @CrossOrigin(origins = "http://localhost:3000", methods = { RequestMethod.GET, RequestMethod.POST }) // 클래스 레벨
      public class MyController {
      
          @GetMapping("/data")
          public String getData() {
              return "Hello from Spring!";
          }
      
          @CrossOrigin(origins = "http://specific.domain.com") // 메서드 레벨 (클래스 레벨 설정 오버라이드)
          @GetMapping("/specificData")
          public String getSpecificData() {
              return "Specific data!";
          }
      }
      
    • 장점: 세밀한 제어가 가능하며, 특정 API만 Cross-Origin 요청을 허용해야 할 때 유용합니다.
    • 주의: @CrossOriginPreflight Request를 자동으로 처리합니다.
  3. CorsFilter Bean Registration (별도의 Filter 등록):
    • 위에서 설명한 WebMvcConfigurer 방식은 Spring MVC DispatcherServlet의 범위 내에서 작동합니다. 만약 DispatcherServlet 밖에서 CORS를 처리해야 하거나, Spring Security와 통합하기 위해 더 세밀한 제어가 필요하다면 CorsFilter를 별도의 스프링 빈으로 등록할 수 있습니다.
    • API: org.springframework.web.filter.CorsFilter 클래스와 org.springframework.web.cors.CorsConfigurationSource를 조합하여 빈으로 등록합니다.
    • 이 방법은 Spring Security와 통합될 때 SecurityFilterChain 내에서 CorsFilter를 사용하는 방식과 동일합니다.