이번에 프런트엔드와의 협업을 하다 CORS 설정을 접하게 되었다. 설정은 검색을 해서 해결은 했지만 CORS에 대해 자세히 이해하면 더 좋을 것 같다는 생각을 해 정리를 해보았다.
CORS란?
CORS란 Cross-Origin Resource Sharing의 약자로, 웹 브라우저에서 실행되는 스크립트가 다른 도메인의 리소스에 접근할 수 있도록 하는 보안 기술이다. 보안 정책으로 인해 브라우저는 동일 출처 정책(Same-Origin Policy)을 따르며, 이는 웹 페이지에서 로드된 문서나 스크립트가 동일한 출처(프로토콜, 호스트, 포트가 동일)에서만 리소스에 접근할 수 있다는 것을 의미한다.
동일 출처 정책에 위반되는 경우
1. 프로토콜 - http와 https는 프로토콜이 다르다.
2. 도메인 - domain.com과 other-domain.com은 다르다.
3. 포트번호 - 8080 포트와 3000 포트는 다르다.
하지만 웹 애플리케이션은 다양한 도메인 간 리소스 공유가 필요한 경우가 많다.(앞선 백엔드와 프런트엔드와의 협업도 한 가지 예시가 될 수 있다.) 이때 필요한 것이 CORS이다. 서버 측에서 특정 도메인에서의 요청을 허용하도록 설정함으로써 브라우저가 해당 리소스에 접근할 수 있게 된다.
CORS가 동작하는 방식
1. 단순요청(Simple Requests)
- 클라이언트는 서버로 요청을 한다.
- 서버의 응답이 왔을 때 브라우저가 요청한 Origin과 응답한 헤더 Access-Control-Request-Headers의 값을 비교하여 유효한 요청이라면 리소스를 응답한다. 만약 유효하지 않은 요청이라면 브라우저에서 이를 막고 에러가 발생한다.
Access-Control-Request-Headers?
브라우저가 실제 요청을 보내기 전에 서버에게 허용을 요청하는 데 사용되는 HTTP 헤더이며, 이 헤더는 실제 요청에서 사용될 수 있는 헤더 목록을 서버에게 알려주는 역할을 한다.
Simple Requests?
CORS (Cross-Origin Resource Sharing) 정책에서 사용되는 개념 중 하나이다. Simple Requests는 브라우저가 사전 요청 (Preflight Request) 없이 직접 리소스에 접근할 수 있는 특정 조건을 만족하는 HTTP 요청을 말한다.
Simple Requests가 되기 위해서는 다음의 조건을 모두 충족해야 한다.
- HTTP 메서드: 요청은 GET, HEAD, POST 중 하나의 메서드를 사용해야 한다.
- 사용 가능한 헤더: 사용 가능한 헤더는 다음과 같다.
- Accept
- Accept-Language
- Content-Language
- Content-Type (만약 설정된 경우, "application/x-www-form-urlencoded", "multipart/form-data", 또는 "text/plain" 중 하나여야 함)
- 사용 가능한 컨텐츠 타입: Content-Type 헤더가 사용된 경우, 다음 중 하나여야 합니다.
- "application/x-www-form-urlencoded"
- "multipart/form-data"
- "text/plain"
만약 위와 같은 요청이 모두 만족한다면, 브라우저는 사전 요청을 거치지 않고 해당 리소스에 접근할 수 있다. 이로써 간단한 요청에 대해 추가적인 네트워크 오버를 감소시킬 수 있다.
2. 프리 플라이언트(Preflight Request)
복잡한 요청이나 특정 헤더를 사용하는 요청의 경우 사전 요청 형태로 서버로 보낸다. 이 사전 요청은 HTTP메서드가 OPTIONS인 요청으로 전달되며, 서버에서 실제 요청 전에 허용 여부를 확인하고자 하는 것이다.(이때 내용물은 없이 헤더만 전송한다.)
Preflight Request?
요청 헤더에는 다음 값이 포함된다.
- origin : 어디서 요청을 했는지 서버에 알려주는 주소
- access-control-request-method : 실제 요청이 보낼 HTTP 메서드
- access-control-request-headers : 실제 요청에 포함된 header
응답 헤더에는 다음 값이 포함된다.
- access-control-allow-origin : 서버가 허용하는 출처
- access-control-allow-methods : 서버가 허용하는 HTTP 메서드 리스트
- access-control-allow-headers : 서버가 허용하는 header 리스트
- access-control-max-age : 프리 플라이트 요청의 응답을 캐시에 저장하는 시간
백엔드 적용(Spring Boot)
1. 전역 설정하기
CORS 정책의 설정은 WebMvcConfigurer를 구현하여 설정할 수 있다. 필터 기반이기 때문에, 전역적으로 모든 요청에 대해 검사한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
// 허용 Orgin
.allowedOrigins("http://allowed-origin.com")
// 허용 메서드
.allowedMethods("GET", "POST")
// 허용 헤더
.allowedHeaders("header1", "header2")
// 쿠키 공유 허용
.allowCredentials(true);
}
}
2. 개별 적용시키기
@CrossOrigin은 스프링 4.2 버전부터 제공하며 CORS를 스프링을 통해 설정할 수 있는 기능이다. @CrossOrigin 어노테이션을 붙여주면 기본적으로 '모든 도메인, 모든 요청방식'에 대해 허용한다는 의미이다.
@RestController
@CrossOrigin(origins = "http://allowed-origin.com", methods = {RequestMethod.GET, RequestMethod.POST})
public class MyController {
@CrossOrigin("http://allowed-origin.com")
@RequestMapping(method = RequestMethod.GET, "/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
}
위와 같이 Controller에 적용해도 되고 Method에 붙여 사용하여도 된다.
3. Spring Security를 사용한다면?
시큐리티를 사용한다면 시큐리티 필터를 타게되는 이슈가 발생되게 된다. 그렇기 때문에 시큐리터 설정 내 추가적으로 CORS 설정을 해주게 된다.
@Configuration
@EnableWebSecurity
public class SecurityConfig{
[...]
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web ->
web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
.requestMatchers("/favicon.ico", "/resources/**", "/error");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.....
.cors(httpSecurityCorsConfigurer ->
httpSecurityCorsConfigurer
.configurationSource(corsConfigurationSource())
)
.....
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/users").authenticated()
.requestMatchers("/payment/instant").authenticated()
.requestMatchers("/payment/purchase").authenticated()
.requestMatchers(HttpMethod.GET, "/payment").authenticated()
.requestMatchers("/carts").authenticated()
.anyRequest().permitAll()
)
.....
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
// 쿠키 사용 허용
config.setAllowCredentials(true);
// 오리진 허용
config.setAllowedOrigins(List.of("http://localhost:5173", "https://localhost:5173",
"https://domain.com"));
// 메서드 허용
config.setAllowedMethods(
Arrays.asList(HttpMethod.POST.name(), HttpMethod.GET.name(),
HttpMethod.PUT.name(), HttpMethod.DELETE.name(),
HttpMethod.OPTIONS.name())
);
// 요청헤더 허용
config.setAllowedHeaders(
Arrays.asList("Authorization", ...)
);
// 응답 헤더 허용
config.setExposedHeaders(
Arrays.asList("Content-Type", ...)
);
UrlBasedCorsConfigurationSource source
= new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean
= new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return source;
}
}
시큐리티 필터를 타느냐 아니냐의 기준?
위 시큐리티 설정 클래스를 보면 "/favicon.ico", "/resources/**"등과 같은 리소스 경로들은 시큐리티 필터에서 제외하도록 web.ignoring(). antmatchers()안에 설정을 해두었다. (경우에 따라 정적 리소스 말고도 api URL로 설정되기도 한다.)
이처럼 시큐리티 필터를 타지 않는 요청의 경우 스프링 전역 설정(1번)에 영향을 받는다.
이 외에 시큐리티 필터를 타는 요청의 경우 스프링 시큐리티 설정(위 코드)에 영향을 받는다.
그렇기때문에 시큐리티 제외 요청에서 CORS 에러가 난다면 WebMvcConfigurer 구현 클래스에서 설정을 해야 하고,
시큐리티 필터 요청에서 CORS 에러가 난다면 스프링시큐리티 설정클래스에서 설정을 해야 한다.
이 외에도 컨트롤러에 직접 CORS를 설정하여 적용해 주어도 된다.
더 자세한 설정일수록 우선순위를 갖기 때문이다.
참고
https://escapefromcoding.tistory.com/724
https://hannut91.github.io/blogs/infra/cors
https://henniee.tistory.com/199
https://ktae23.tistory.com/222
'Knowledge > 네트워크' 카테고리의 다른 글
[네트워크] 네트워크와 인터넷, 네트워크의 기본구조 (0) | 2024.05.09 |
---|---|
[네크워크] HTTP Status Code(HTTP 상태 코드) (1) | 2023.12.14 |
[네트워크] 세션(Session) vs 쿠키(Cookie) vs 토큰(Token) (0) | 2023.12.07 |
[스친초5기/네트워크] HTTP와 HTTP 메서드 (0) | 2023.11.07 |
[네트워크] 인터넷과 웹의 관계 (1) | 2023.10.23 |