CORS (Cross-Origin Resource Sharing): 개념, 작동 방식, Spring Security에서의 처리
1. CORS란 무엇이며, 왜 나왔는가?
**CORS (Cross-Origin Resource Sharing)**는 웹 페이지의 제한된 리소스가 다른 도메인으로부터 요청될 수 있도록 허용하는 메커니즘입니다. 즉, 웹 브라우저가 다른 출처(Origin)의 리소스에 접근할 수 있도록 허용하는 표준 방식입니다.
왜 나왔는가? (Same-Origin Policy의 등장과 한계)
웹 보안의 근간에는 Same-Origin Policy (동일 출원 정책)가 있습니다. 이 정책은 한 출처에서 로드된 웹 페이지가 다른 출처의 리소스와 상호작용하는 것을 제한하여 악성 웹사이트가 사용자의 데이터를 훔치거나 제어하는 것을 방지합니다.
- 출처(Origin)의 정의: 프로토콜(protocol), 호스트(host), 포트(port) 세 가지가 모두 동일해야 같은 출처로 간주됩니다. 이 중 하나라도 다르면 다른 출처로 간주됩니다.
- 예시:
- http://example.com:8080/path
- http://example.com:80/path (포트 다름) -> 다른 출처
- https://example.com:8080/path (프로토콜 다름) -> 다른 출처
- http://sub.example.com:8080/path (호스트 다름) -> 다른 출처
- 예시:
문제는 웹 서비스가 발전하면서 프론트엔드(예: 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 안전 헤더 외의 다른 커스텀 헤더를 사용하지 않아야 합니다.
작동 방식:
- 클라이언트 (브라우저): CORS 헤더(Origin 등)를 포함하여 요청을 서버로 직접 전송합니다.
- 서버: 요청을 처리하고, Access-Control-Allow-Origin 등 CORS 응답 헤더를 포함하여 응답을 보냅니다.
- 클라이언트 (브라우저): 서버 응답의 CORS 헤더를 확인합니다. Origin이 허용되면 응답을 애플리케이션으로 전달하고, 허용되지 않으면 에러를 발생시킵니다.
2) Preflight Request (사전 요청)
Simple Request의 조건을 만족하지 않는 복잡한 요청(예: PUT, DELETE 메서드 사용, 커스텀 헤더 포함, application/json Content-Type 사용 등)은 실제 요청을 보내기 전에 브라우저가 서버에게 "이러한 요청을 보내도 괜찮을까요?"라고 묻는 OPTIONS 메서드의 사전 요청을 먼저 보냅니다.
작동 방식:
- 클라이언트 (브라우저): 실제 요청을 보내기 전에 OPTIONS 메서드를 사용하여 Preflight Request를 서버로 전송합니다. 이 요청에는 실제 요청에 포함될 HTTP 메서드(Access-Control-Request-Method)와 헤더(Access-Control-Request-Headers) 정보가 포함됩니다.
- 서버: Preflight Request를 수신하고, 실제 요청이 허용 가능한지 여부를 판단합니다. 허용 가능한 경우, Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age (사전 요청 결과를 캐싱할 시간) 등의 CORS 응답 헤더를 포함하여 응답을 보냅니다. 이 응답에는 실제 요청의 데이터는 포함되지 않습니다.
- 클라이언트 (브라우저): Preflight Request에 대한 서버의 응답을 확인합니다.
- 허용되면: 실제 요청(본 요청)을 다시 서버로 전송합니다.
- 허용되지 않으면: 실제 요청을 보내지 않고 CORS 에러를 발생시킵니다.
- 서버 (두 번째 요청): 실제 요청을 처리하고 응답을 반환합니다.
Preflight Request는 Cross-Origin 통신 시 보안을 한 번 더 강화하는 역할을 합니다. 서버가 허용하지 않는 요청이 실수로 데이터를 변경하는 등의 부작용을 사전에 방지할 수 있습니다.
3. Spring Security에서 CORS 처리 방법
Spring Security는 웹 보안 프레임워크이므로 CORS 처리와 밀접하게 관련되어 있습니다. Spring Security 5.x 버전부터는 CORS를 직접 지원하며, WebSecurityConfigurerAdapter (최신 버전에서는 SecurityFilterChain 빈 구성)를 통해 설정을 간편하게 할 수 있습니다.
Spring Security가 CORS를 처리하는 주요 방법은 다음과 같습니다.
- CorsFilter: Spring Security 필터 체인에 CorsFilter를 추가하여 CORS 요청을 처리합니다. 이 필터는 Preflight Request와 Simple Request 모두를 가로채어 적절한 CORS 헤더를 추가하거나 요청을 거부하는 역할을 합니다. CorsFilter는 일반적으로 Spring Security 필터 체인의 초기에 위치하여 모든 요청에 대해 CORS 검사를 수행합니다.
- CorsConfigurationSource: CorsFilter는 CorsConfigurationSource를 사용하여 어떤 출처, 메서드, 헤더 등을 허용할지 결정합니다. 개발자는 이 소스를 통해 세부적인 CORS 정책을 정의할 수 있습니다.
예시 (Spring Security 6.x 이상, Java Config):
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와 방법을 제공합니다.
- 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 요청 캐싱 시간 } }
- 장점: 중앙 집중식으로 관리되어 편리합니다.
- 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 요청을 허용해야 할 때 유용합니다.
- 주의: @CrossOrigin은 Preflight Request를 자동으로 처리합니다.
- 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를 사용하는 방식과 동일합니다.