교차 출처 리소스 공유 (다른 출처의 리소스를 공유)
여기서 말하는 출처는 protocol, host, port를 합친 것을 의미
웹 생태계의 보안 정책으로 "같은 출처에서만 리소스를 공유할 수 있다"라는 정책
Postman이나 ARC로 API 테스트를 했을 때는 동일 출처이기 때문에 문제가 없다.
하지만 React App과 Back Spring App을 돌리면 다음과 같은 문제가 발생한다.
=> Spring App의 Port는 8080인데 React App은 3000번 Port를 사용하기 때문에 브라우저에서 다른 출처를 허용하지 않아 발생하는 문제
이러한 SOP 정책이 등장한 이유는 서로 다른 두 개의 어플리케이션이 마음대로 소통할 수 있다면, 보안상 매우 위험하기 때문이다. 브라우저의 개발자 도구창만 열어봐도 어떤 서버와 통신하는지, 리소스의 출처가 어딘지 쉽게 열람할 수 있기 때문에, 다른 출처의 어플리케이션이 서로 통신하는 것에 제약이 없으면, CSRF나 XSS와 같은 공격으로 정보 탈취가 쉬워진다.
하지만 웹에서는 다른 출처의 리소스를 가져와 사용하는 일이 빈번하다.
따라서 몇 가지 예외 조항을 두고 출처가 다른 리소스 요청도 허용할 수 있도록 해주었는데, 그것이 바로 CORS 정책을 지킨 리소스 요청이다.
출처를 비교하는 곳은 사실 서버가 아니라 브라우저에서 구현된다.
그래서 CORS 정책을 위반하는 요청을 하더라도 서버에서는 정상적으로 응답을 하지만, 브라우저에서 그 응답을 분석하고 출처를 비교하여 CORS 정책에 위반된다면 해당 응답을 버려버린다.
=> 이 문제를 해결하기 위해서는 Back이나 Front 둘 중에 한 군데에서 CORS 설정을 해주면 된다.
단순 요청 방법은 서버에게 그냥 바로 요청을 보내는 방법이다.
서버에 API를 요청하고 서버에서는 Access-Control-Allow-Origin 헤더를 포함한 응답을 브라우저에게 보내면 브라우저는 해당 헤더를 확인하고 CORS를 수행할지 판단한다.
Simple Request의 조건은 아래와 같다.
=> 이 조건들은 REST API를 사용한다면 만족하기 어렵다
( 많은 REST API들이 Content-Type을 application/json로 사용하기 때문이다 )
예비 요청을 먼저 보내는 방법은 예비 요청을 보내서 안전한지 판단한 후 본 요청을 보내는 방법이다.
OPTIONS 메서드로 서버에 예비 요청을 보낸 다음 서버는 응답으로 Access-Control-Allow-Origin 헤더를 포함한 응답을 브라우저에 보낸다.
브라우저는 Access-Control-Allow-Origin 헤더를 확인해서 CORS 동작을 수행할지 판단한다.
후에 확인이 완료되었다면 본 요청인 GET 요청을 통해 리소스를 받아온다.
스프링 부트에서 CORS를 해결하는 방법은 4가지 정도가 있다.
Spring Security corsConfigurationSource
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// ... 중략
@Override
protected void configure(HttpSecurity http) throws Exception {
// cors설정 추가
http
.cors()
.configurationSource(corsConfigurationSource());
// ... 중략
}
// ... 중략
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000"); // local 테스트 시
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.addExposedHeader("Authorization");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}