3 분 소요

CORS의 두 가지 검증 계층

CORS는 브라우저와 서버 양쪽에서 각각 독립적으로 동작하는 검증 메커니즘을 가진다.

1.1. 브라우저의 CORS 정책

  • 다른 출처 요청 시에만 동작
  • Preflight 요청(OPTIONS)을 통한 사전 확인
  • 서버의 CORS 응답 헤더를 검증
  • 부적절한 응답 시 JavaScript에서 요청 차단

1.2. 서버의 CORS 정책 (Spring 기준)

  • 모든 요청에 대해 origin 검증 가능
  • WebMvcConfigurer를 통한 설정
  • 허용되지 않은 origin 요청을 403으로 차단
  • 브라우저의 CORS 정책과 무관하게 동작

2. 실제 동작 사례

2.1. 다른 출처 요청의 경우

브라우저 (http://localhost:3000) → API 서버 (http://localhost:8080)

1. 브라우저 CORS 정책 적용
- Preflight 요청 발생
- CORS 헤더 검증

2. 서버 CORS 정책 적용
- WebMvcConfigurer 설정에 따른 origin 검증
- allowedOrigins 목록 확인

2.2. 같은 출처 요청의 경우

브라우저 (https://apiloan.p-e.kr) → API 서버 (https://apiloan.p-e.kr)

1. 브라우저 CORS 정책 미적용
- 같은 출처이므로 Preflight 요청 없음
- CORS 헤더 검증 안 함

2. 서버 CORS 정책 적용
- WebMvcConfigurer 설정이 있다면 여전히 origin 검증
- allowedOrigins에 없으면 403 반환

3. Spring의 CORS 구현

서버에서는 브라우저의 요청에 대한 응답을 할 때 CORS 관련 헤더에 허가 정보를 담아 보낸다. 단순히 허가 정보를 담아 응답을 할 수도 있다.

// 헤더만 추가하고 실제 검증은 안함
response.setHeader("Access-Control-Allow-Origin", "*");

추가로 서버에서도 SpringSecurity 또는 WebMvcConfigurer 설정을 통해 CORS 검증을 통해 허용된 경우 응답하는 방식도 있다.

WebMvcConfigurer 설정

서버 측 보안 정책으로 동작하며, 브라우저의 CORS 상태와 무관하게 origin 등을 검증한다. 또한 모든 HTTP 요청에 대해 검증 수행

WebMvcConfigurer는 MVC 요청 처리 단계에서 동작한다.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
      registry.addMapping("/**") // 모든 경로에 대해 CORS 허용
              .allowedOrigins("https://allowed-origin.com") // 프론트엔드 도메인 허용
              .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드
              .allowedHeaders("*"); // 허용할 헤더         
    }
}

addMapping

  • 요청 경로가 addMapping에 정의된 경로와 일치하는 지 검증한다.

allowedOrigins

  • 요청 Origin 값이 allowedOrigins에 정의된 주소와 동일한 지 검증한다.
  • allowedOrigins에 정의된 값과 일치하지 않는 경우 403 Forbidden을 반환한다.

allowedMethods

  • 요청 http 메서드가 allowedMethods에 정의된 값에 포함되는 지 검증한다.

allowedHeaders

  • 요청 헤더 중 allowedHeaders에 정의된 값에 포함되는 지 검증한다. allowedHeaders에는 허용 할 헤더를 정의한다.

위 검증 과정을 통과하면 응답에 CORS 헤더(Access-Control-Allow-Origin, Access-Control-Allow-Methods 등)를 추가한다.

주요 특성

  • 서버 측 보안 정책으로 동작
  • 브라우저의 CORS 상태와 무관하게 origin 검증
  • 모든 HTTP 요청에 대해 검증 수행

SpringSecurity 설정

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
      CorsConfiguration configuration = new CorsConfiguration();
      configuration.addAllowedOrigin("http://allow-origin.com"); // 허용할 출처
      configuration.addAllowedMethod("*"); // 모든 HTTP 메서드 허용
      configuration.addAllowedHeader("*"); // 모든 헤더 허용
      configuration.setAllowCredentials(true); // 쿠키/인증정보 허용

      UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
      source.registerCorsConfiguration("/**", configuration);
      return source;
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
      .cors(cors -> cors.configurationSource(corsConfigurationSource()))
      .csrf(csrf -> csrf.disable())
      .sessionManagement(session -> 
          session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      
      .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), 
          UsernamePasswordAuthenticationFilter.class)
      
      .authorizeHttpRequests(auth -> auth
          .requestMatchers(
              "/api/v1/auth/**",
              "/swagger-ui/**",
              "/v3/api-docs/**",
              "/docs/**"
          ).permitAll()
          .anyRequest().authenticated())
      
      .exceptionHandling(exceptionHandling -> 
          exceptionHandling.authenticationEntryPoint(
              new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
      .build();
  }
}

1. Security Filter Chain에서 CORS 필터 적용

  • Spring Security는 요청을 처리하기 전, Security Filter Chain에서 CorsFilter를 호출한다.
  • CorsFilter는 등록된 CorsConfigurationSource에서 설정된 정책에 따라 요청을 검증한다.

2. CORS 정책 검증

  • 출처 검증: 요청의 Origin 헤더가 addAllowedOrigin에 등록된 값(http://allow-origin.com)과 일치하는지 확인한다.
    • 일치하지 않으면 요청이 차단된다.
  • HTTP 메서드 검증: 요청의 HTTP 메서드가 addAllowedMethod("*")에 포함된 메서드인지 확인한다.
  • 헤더 검증: 요청의 Access-Control-Request-Headers에 포함된 헤더가 addAllowedHeader("*")에 허용된 헤더인지 확인한다.
  • 쿠키/인증정보 검증: 요청에 withCredentials: true가 포함된 경우, 서버가 setAllowCredentials(true)로 설정되어 있는지 확인한다.
    • 설정되어 있지 않으면 요청이 차단됩니다.

3. 응답 헤더 추가

  • 요청이 검증을 통과하면, 서버는 응답에 다음과 같은 CORS 헤더를 추가한다.
    • Access-Control-Allow-Origin: 허용된 출처.
    • Access-Control-Allow-Methods: 허용된 HTTP 메서드.
    • Access-Control-Allow-Headers: 허용된 요청 헤더.
    • Access-Control-Allow-Credentials: 인증 정보를 허용했는지 여부.

4. 검증 실패 시

  • 요청이 검증을 통과하지 못하면 Spring Security는 요청을 차단하며, 403 Forbidden 응답을 반환한다.

정리하면 CORS 검증은 Spring Security의 CorsFilter에서 수행되며, 설정된 CorsConfigurationSource에 따라 요청을 검증한다. CORS 검증에 실패하면 요청이 Security Filter Chain의 나머지 부분에 도달하지 못하고 차단된다.

두 방식의 매커니즘 차이

특징 WebMvcConfigurer만 사용 Spring Security와 함께 사용
CORS 처리 위치 Spring MVC 요청 처리 단계에서 CORS 검증 수행 Security Filter Chain 단계에서 CORS 검증 수행
검증 적용 대상 컨트롤러(/api/** 등)로 매핑된 요청만 검증 모든 HTTP 요청 검증 가능 (컨트롤러 외 요청 포함)
Preflight 요청 처리 자동 처리 (Spring MVC에서 OPTIONS 요청 처리) 자동 처리, Security Filter Chain에서 처리 가능
우선순위 Spring Security 에 설정 없으면 동작 Spring Security 설정이 있으면 WebMvcConfigurer 무시
유연성 단순한 CORS 정책 설정에 적합 더 세밀한 보안 및 CORS 정책 설정 가능

SpringSecurity + WebMvcConfigurer 설정

두 설정을 혼합하여 사용할 수 있다.

  • WebMvcConfigurer 설정
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
          registry.addMapping("/**")
                  .allowedOrigins("https://allowed-origin.com")
                  .allowedMethods("*");
      }
    }
    
  • SpringSecurity 설정에서 cors 설정을 WebMvcConfigurer에 위임한다.
    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      return http
        .cors(cors -> {}) // WebMvcConfigurer로 CORS 위임
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session -> 
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
          
        .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), 
            UsernamePasswordAuthenticationFilter.class)
          
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(
                "/api/v1/auth/**",
                "/swagger-ui/**",
                "/v3/api-docs/**",
                "/docs/**"
            ).permitAll()
            .anyRequest().authenticated())
          
        .exceptionHandling(exceptionHandling -> 
            exceptionHandling.authenticationEntryPoint(
                new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
        .build();
    }
    }
    

태그: ,

카테고리:

업데이트:

댓글남기기