브라우저는 SOP( Same-Origin Policy ) 에 의해 서로다른 Origin 간에 리소스 공유를 금지한다.
여기서 Origin은 URI의 Scheme, 호스트, 포트가 모두 동일해야 동일한 Origin 으로 취급된다.
즉, 스크립트를 통해 서로다른 웹 페이지간에 민감한 정보에 접근할 수 없다는 말이다.
CORS( Cross-Origin Resource Sharing )는 이러한 SOP를 특정 Origin을 대상으로는 완화해주는 설정을 의미한다.
기본적으로 CORS 에러를 판별하는 주체는 브라우저다.
- 요청 헤더에
Origin으로 현재 출처를 표시하여 요청은 전송- 서버에서
Access-Control-Allow-Origin헤더로 접근이 허용된 출처를 안내- 브라우저에서 유효한 응답인지를 결정한다.
의 절차를 거쳐 CORS 에러를 발생시킬지 결정한다.
CORS 접근이 허용된 요청이 아닌경우, 403 Forbidden 응답이 반환된다.
MDN 문서에서 소개하는 세 가지의 요청 시나리오를 살펴보자

요청 메서드가 GET, HEAD, POST 중 하나이면서,
요청 헤더에는 Accept, Accept-Language, Content-Language, Content-Type 가 허용된다.
(단, Content-Type의 값은 application/x-www-form-urlencoded, multipart/form-data, text/plain 이어야 함)
위 조건에 맞게 Origin 헤더를 포함한 요청을 보내면 서버측에서 CORS설정과 대조하여 응답을 내려준다
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
예를들어, 위 요청은 https://foo.example에서 bar.other/resources/public-data/ 로의 GET 요청을 의미한다.
단순 요청의 조건을 모두 충족했으므로, 시나리오에 따라 아래와 같이 응답이 도착한다
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[…XML Data…]
이제 브라우저에서 Origin과 응답의 Access-Control-Allow-Origin를 대조하여 CORS 에러를 발생시킬지 결정한다.

요청 메서드가 OPTIONS 이면서,
요청 헤더에 Origin와Access-Control-Request-Method 가 포함된 요청을 사전요청(pre-flight) 이라 부른다.
실제 요청이전에 사전요청을 보내어 실제 요청이 가능한지를 판단하고, 가능한 상황에서만 실제 요청을 서버로 전송한다.
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example // 요청 주체
Access-Control-Request-Method: POST // 실제 요청 메서드
Access-Control-Request-Headers: content-type,x-pingother // 실제 요청에 포함될 헤더
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example // 허용하는 Origin
Access-Control-Allow-Methods: POST, GET, OPTIONS // 허용하는 메서드
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type // 허용하는 헤더
Access-Control-Max-Age: 86400 // 현재 사전요청의 캐시기간
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
예비요청과 그에대한 응답을 나타내는 예시이다.
응답에 포함된 정보들이 실제 요청과 일치하면 서버로 실제 요청을 보낸다.

Credentials을 포함한 요청은 쿠키나 Authorization 헤더가 포함된 요청을 의미한다.
서버측에서 응답헤더에 Access-Control-Allow-Credentials: true를 포함하지 않으면 응답은 무시된다.
credential이 포함된 요청에 응답할 때는 아래 헤더들에 *를 지정해서는 안된다.
Access-Control-Allow-OriginSet-Cookie가 쿠키를 설정하지 않는다Access-Control-Allow-Headers Access-Control-Allow-Methods Access-Control-Expose-HeadersSpring MVC에서 CORS관련 동작을 시작하는 클래스는 CorsFilter이며,
실제 동작은 DefaultCorsProcessor에서 이뤄진다.
동작과정에서 CORS 설정을 읽어와 CorsConfiguration 타입의 인스턴스를 생성하는데,
이 인스턴스를 생성하는데 관여하기 위해서는 @CrossOrigin 혹은 전역 설정이 필요하다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
@AliasFor("origins")
String[] value() default {};
@AliasFor("value")
String[] origins() default {};
String[] originPatterns() default {};
String[] allowedHeaders() default {};
String[] exposedHeaders() default {};
RequestMethod[] methods() default {};
String allowCredentials() default "";
String allowPrivateNetwork() default "";
long maxAge() default -1;
}
이 애노테이션은 컨트롤러나 컨트롤러 메서드에 설정할 수 있다.
기본적으로 모든 Origin과 Header, Method를 허용하며, 자격증명을 포함한 요청은 기본적으로 비활성화 되어있다.
maxAge는 30분으로 설정되어있다.
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
전역 설정은 기본적으로 모든 Origin과 Header를 허용하며, 메서드는 GET, HEAD, POST만 허용한다.
자격증명을 포함한 요청은 기본적으로 비활성화 되어있고, maxAge는 30분으로 설정되어있다.
CORS 시나리오 중 하나인 pre-flight 요청에는 쿠키같은 자격증명 정보가 포함되지 않으므로,
CORS는 스프링 시큐리티의 동작전에 동작한다.
스프링 시큐리티에서 사용자는 CorsConfigurationSource를 통해 CorsFilter를 스프링 시큐리티와 통합시킬 수 있다.
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
스프링 시큐리티는 위 코드처럼 UrlBasedCorsConfigurationSource 인스턴스가 Bean으로 제공되면, 자동으로 CORS 설정을 구성한다.
만약, Spring MVC의 전역 설정이 구성되어있다면 CORS 설정을 생략해도 된다.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
@Order(0)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.cors((cors) -> cors
.configurationSource(apiConfigurationSource())
)
...
return http.build();
}
@Bean
@Order(1)
public SecurityFilterChain myOtherFilterChain(HttpSecurity http) throws Exception {
http
.cors((cors) -> cors
.configurationSource(myWebsiteConfigurationSource())
)
...
return http.build();
}
CorsConfigurationSource apiConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://api.example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
CorsConfigurationSource myWebsiteConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
또한, 다수의 CorsConfigurationSource Bean을 등록한다면 .cors() DSL로 동작할 CORS 설정을 지정해줘야한다.