추가 HTTP 헤더를 사용하여, 한 출처(Origin)에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.
교차 출처 요청의 예시: https://domain-a.com의 프론트 엔드 JavaScript 코드가 XMLHttpRequest를 사용하여 https://domain-b.com/data.json을 요청하는 경우
보안 상의 이유로, 브라우저는 스크립트에서 시작한 교차 출처 HTTP 요청을 제한한다. 웹브라우저는 동일 출처 정책(Same Origin Policy) 을 따른다 즉 자신의 출처와 동일한 리소스만 불러올 수 있으며, 다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 한다.
오해하는 부분이 있는데 출처 구분을 서버가 하는 것이 아니다. 출처를 비교하는 로직은 서버에 구현된 스펙이 아닌 브라우저에 구현된 스펙이다.
Protocol + Host + Port 3가지가 같으면 동일 출처(Same Origin)라고 한다.
cross-origin이란 다음 중 한 가지라도 다른 경우를 말한다.
단순 요청은 말그대로 예비 요청(Prefilght)을 생략하고 바로 서버에 직행으로 본 요청을 보낸 후, 서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin 헤더를 보내주면 브라우저가 CORS정책 위반 여부를 검사하는 방식이다.
아래의 조건에 부합하는 요청은 추가적으로 확인하지 않고 바로 본 요청을 보낸다.
OPTIONS HttpMethod를 통해 다른 도메인의 리소스로 HTTP 요청을 보내 실제 요청이 전송하기에 안전한지 확인한 후 본 요청을 보낸다.
요청 헤더의 Origin과 응답 헤더의 Access-Control-Allow-Origin 의 URL값이 서로 같을 경우 다른 출처라도 cors가 허용되어 본 요청을 보내고 정상 응답을 받는다.
만일 다를 경우 브라우저는 CORS 정책을 위반 했다 판단하여 에러를 출력한다.
본 요청을 보내기 전 예비 요청을 보내 보안을 강화하는 목적의 취지는 좋으나 실제 요청에 걸리는 시간이 늘어나게 되어 어플리케이션 성능에 영향을 미치는 단점이 존재한다.
Preflight Request 헤더 목록
Preflight Request 의 서버 response 헤더 목록
Access-Control-Allow-Origin, Headers, Methods, Access-Control-Expose-Headers 4개의 헤더 모두 자격증명(credentials) 이 없는 요청의 경우에만 '' 와일드 카드로서의 의미가 있다.
자격증명이 있는 요청에 'Access-Control-Allow-Origin: ' 지정할 경우 에러가 발생하며
나머지 3개 헤더는 리터럴 의미의 값인 *
이 지정될뿐 와일드 카드로서의 기능은 하지 못한다.
Access-Control-Allow-Origin : 헤더의 값으로 지정된 도메인으로부터의 요청만 서버의 리소스에 접근할 수 있게 한다.
단일 Origin만 지정이 가능하다. 만약 서버가 여러개의 Origin을 지원할 경우 Origin 헤더에 값을 확인하여 지원하는 Origin일 경우 해당 Origin만 Access-Control-Allow-Origin 헤더에 담아야 한다.
예) 서버가 http://test.com, http://example.com 2개의 Origin을 지원할 경우 http://test.com Origin 으로 부터 Preflight Request 가 왔다면 Access-Control-Allow-Origin 헤더에 http://test.com Origin만 설정해야 한다.
Access-Control-Expose-Headers : 서버가 반환할 HTTP 응답 헤더 중에서 브라우저가 접근할 수 있도록 노출할 헤더를 알려주는 역할을 한다. 브라우저는 이 헤더에 명시된 HTTP 응답 헤더에 접근할 수 있다.
브라우저는 보안상의 이유로 Cross-Origin 요청에 대해서는 서버의 응답 헤더 중 일부에만 접근할 수 있도록 제한한다.
따라서, 서버가 클라이언트 애플리케이션에서 접근할 수 있도록 허용하는 특정 커스텀 헤더를 Access-Control-Expose-Headers 헤더로 설정해주어야 한다.
예를 들어 서버에서 반환하는 HTTP 응답 헤더 중에서 Authorization 헤더가 포함되어 있을 때, 이 헤더는 보안상의 이유로 브라우저에서는 접근할 수 없다.
그러나 Access-Control-Expose-Headers 헤더에 Authorization 헤더를 명시하면, 서버에서 반환된 HTTP 응답 헤더의 Authorization 값을 JavaScript 코드에서 접근할 수 있게 된다.
즉 해당 헤더를 사용하면 서버에서 반환되는 HTTP 응답 헤더 중에서 브라우저가 접근할 수 있는 헤더를 선택하여 노출시킬 수 있으며, 브라우저는 이를 사용하여 추가적인 처리를 수행할 수 있다.
기본적으로 브라우저에게 노출이 되는 HTTP Response Header는 6가지 밖에 없다.
Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
Access-Control-Max-Age : Preflight 방식을 사용하면 요청을 두번 보내야해서 리소스를 많이 사용하게 된다.
이를 효율적으로 관리하고자 브라우저에서는 첫 요청에서 캐시를 만들어 저장하고 다음 번에 보낼 때는 캐시를 확인하고 본 요청 한번만 보내는 방식을 사용한다.
Access-Control-Max-Age는 서버에서 브라우저에게 이 시간만큼 캐시를 생성하라는 명령으로 초 단위로 지정한다.
Firefox는 최대 24시간(86400초), 크롬(v76 이전 버전)은 최대 10분(600초), 크롬(v76부터)은 최대 2시간(7200초)으로 제한이 있으며 기본값은 5초이다.
또한 preflight response의 응답 코드는 200대여야만 하고 body는 비어있는 것이 좋다.
⦁ Access-Control-Allow-Credentials : 서버가 cross-origin HTTP request 에서 credentials 포함할 수 있는지 여부를 브라우저에게 알려주는 헤더이다.
Credentials 란 cookies, TLS client certificates, username 그리고 password 정보가 포함되어 있는 인증 헤더를 말한다. 기본적으로 이러한 Credentials는 cross-origin request 에서 전송되지 않는다.
Simple Request에 withCredentials = true가 지정되어 있는데, Response Header에 Access-Control-Allow-Credentials: true가 명시되어 있지 않다면, 그 Response는 브라우저에 의해 무시한다.
preflight request 에서는 credentials가 포함되지 않는다. 서버가 preflight request에 대한 응답에 Access-Control-Allow-Credentials 헤더를 true로 설정하면 실제 요청에 자격 증명이 포함된다. 그렇지 않으면 브라우저에서 네트워크 오류를 보고한다.
본 요청에서는 credentials를 포함하는데 만약 Access-Control-Allow-Credentials 헤더를 true로 설정하지 않은 경우 브라우저에서 네트워크 오류를 보고한다.
⦁ Access-Control-Allow-Methods : 예비 요청에 대한 Response Header에 사용되며 서버의 리소스에 접근할 수 있는 HTTP Method 방식을 지정한다.
⦁ Access-Control-Allow-Headers : 예비 요청에 대한 Response Header에 사용되며, 본 요청에서 사용할 수 있는 HTTP Header를 지정한다.
Authorization 헤더는 와일드카드로 지정할 수 없으며 항상 명시적으로 설정해야 한다.
◉ 예비 요청의 문제점과 캐싱
⦁ 예비 요청을 보내 보안을 강화하는 목적의 취지는 좋으나 실제 요청에 걸리는 시간이 늘어나게 되어 어플리케이션 성능에 영향을 미치는 크나큰 단점이 있다.
따라서 브라우저 캐시를 이용해 ccess-Control-Max-Age 헤더에 캐시될 시간을 명시해 주면, 이 Preflight 요청을 캐싱 시켜 최적화를 시켜줄 수 있다.
---캐싱 매커니즘 동작과정
⦁ 1. 브라우저는 예비(Preflight) 요청을 할 때마다, 먼저 Preflight 캐시를 확인하여 해당 요청에 대한 응답이 있는지 확인한다.
2. 만일 응답이 캐싱 되어 있지 않다면, 서버에 예비 요청을 보내 인증 절차를 밟는다.
3. 만일 서버로 부터 Access-Control-Max-Age 응답 헤더를 받는다면 그 기간 동안 브라우저 캐시에 결과를 저장한다.
4. 다시 요청을 보내고 만일 응답이 캐싱 되어 있다면, 예비 요청을 서버로 보내지 않고 대신 캐시된 응답을 사용한다.
인증된 요청 (Credentialed Request)
인증된 요청은 클라이언트에서 서버에게 자격 인증 정보(Credential)를 실어 요청할때 사용되는 요청이다.
자격 인증 정보란 세션 ID가 저장되어있는 쿠키(Cookie) 혹은 Authorization 헤더에 설정하는 토큰 값 등을 일컫는다.
즉, 클라이언트에서 일반적인 JSON 데이터 외에도 쿠키 같은 인증 정보를 포함해서 다른 출처의 서버로 전달할때 CORS의 세가지 요청중 하나인 인증된 요청으로 동작된다는 말이며, 이는 기존의 단순 요청이나 예비 요청과는 살짝 다른 인증 형태로 통신하게 된다.
기본적으로 브라우저가 제공하는 요청 API 들은 별도의 옵션 없이 브라우저의 쿠키와 같은 인증과 관련된 데이터를 함부로 요청 데이터에 담지 않도록 되어있다. 응답을 받을때도 마찬가지 이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 withCredentials 옵션이다.
자바스크립트에서 사용하는 옵션이며 메소드나 라이브러리를 사용하느냐에 따라 withCredentials 옵션을 활성화하는 문법이 다르다.
axios 설정
withCredentials 옵션 부분을 axios 전역 설정으로 처리하거나, axios 요청 메소드의 옵션 인자로 넣어 보낼수 있다.
axios.defaults.withCredentials = true;
fetch 설정
same-origin(기본값) : 같은 출처 간 요청에만 인증 정보를 담을 수 있다. 아래와 같은 이러한 별도의 설정을 해주지 않으면 쿠키 등의 인증 정보는 절대로 자동으로 서버에게 전송되지 않는다.
include : 모든 요청에 인증 정보를 담을 수 있다.
omit : 모든 요청에 인증 정보를 담지 않는다.
jQuery 설정
withCredentials: true
XMLHttpRequest 객체
xhr.withCredentials = true;
credential 정보가 포함되어 있는 요청이 정상적으로 처리되기 위해서는, 해당 요청을 받는 서버 측에서도 다음과 같은 설정이 필요하다.
⦁ 서버도 마찬가지로 이러한 인증된 요청에 대해 일반적인 CORS 요청과는 다르게 대응해줘야 한다.
⦁ 응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정해야 한다.
⦁ 응답 헤더의 Access-Control-Allow-Origin 의 값에 와일드카드 문자("")는 사용할 수 없다.
⦁ 응답 헤더의 Access-Control-Allow-Methods 의 값에 와일드카드 문자("")는 사용할 수 없다.
⦁ 응답 헤더의 Access-Control-Allow-Headers 의 값에 와일드카드 문자("")는 사용할 수 없다.
⦁ 즉, 응답의 Access-Control-Allow-Origin 헤더가 와일드카드()가 아닌 분명한 Origin으로 설정되어야 하고, Access-Control-Allow-Credentials 헤더는 true로 설정되어야 한다는 뜻이다.
위와 같이 설정하지 않으면 cors 정책에 의해 에러가 발생한다.
예비 요청(Preflight)이 필요 없는 단순 요청(Simple Request)의 경우 Access-Control-Allow-Methods 와 Access-Control-Allow-Headers 헤더는 없어도 된다.
◉ CorsConfigurationSource
⦁ CorsConfigurationSource를 Bean으로 등록할 때 Bean 이름은 corsConfigurationSource 이어야만 한다.
해당 메소드 명이 corsConfigurationSource 아닐 경우 @Bean(name ="corsConfigurationSource") 로 설정 해주어야 한다.
그 이유는 CorsConfigurer 클래스에서 CorsFilter를 생성하여 등록하는 방식 때문인데 잠깐 살펴보자
configure(H http) 메소드에 CorsFilter corsFilter = getCorsFilter(context); 로직이 있는데 getCorsFilter 메소드를 보면
boolean containsCorsSource = context.containsBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME);
if (containsCorsSource) { CorsConfigurationSource configurationSource = context.getBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME, CorsConfigurationSource.class);
return new CorsFilter(configurationSource); } 이런 로직이 있는데 CORS_CONFIGURATION_SOURCE_BEAN_NAME 상수를 보면
필드에 private static final String CORS_CONFIGURATION_SOURCE_BEAN_NAME = "corsConfigurationSource"; 이와같이 선언되어 있다.
즉 context.containsBean 메소드로 "corsConfigurationSource" 이름의 Bean이 포함되어 있는지 찾는다 이 때문에 우리가 CorsConfigurationSource 를 Bean으로 등록할 때
Bean 이름을 corsConfigurationSource 으로 등록해야하는 것이다. Bean 이름을 다르게 등록하면 False가 반환되어 필터를 생성하지 못한다.
Bean 이름을 맞게 등록할 경우 True가 반환되어 CorsConfigurationSource 을 가져온 뒤 CorsFilter 인스턴스를 생성하면서 인자값으로 보낸다.
그렇게 되면 CorsFilter 의 private final CorsConfigurationSource configSource; 필드에 대입되어 사용되게 된다.
addAllowedOrigin("Origin1,Origin2,Origin3") 허용 Origin을 추가할 때 ','로 구분하여 여러개 작성이 가능하다.
setAllowCredentials(true) 설정하면 Access-Control-Allow-Credentials 헤더가 True로 설정된다.
addAllowedOrigin("") 로 설정할 경우 Access-Control-Allow-Origin 헤더에 그대로 '' 와일드 카드로 설정된다. setAllowCredentials(true) 설정한 상태에서 요청이 발생할 경우CorsFilter 가 동작할 때 IllegalArgumentException 예외가 발생하는데 그 이유는 Credentials Request의 경우 Access-Control-Allow-Origin 헤더에 '*' 와일드 카드 설정이 불가능 하기 때문이다.
setAllowedOriginPatterns(List.of("*")) 설정할 경우 모든 Origin을 허용할 수 있는데 요청 Origin 헤더의 값을 그대로 Access-Control-Allow-Origin 헤더에 설정하기 때문이다.
그렇기 때문에 Credentials Request 에서도 예외가 발생하지 않고 정상작동 한다.
setAllowedHeaders(List.of("")), setAllowedMethods(List.of("")) 처럼 와일드카드를 설정하더라도 Credentials Request에서 정상적으로 모든 헤더와, 메소드 허용이 가능한데 그 이유는 각 헤더에 직접적으로 '*' 와일드 카드를 설정하는 것이 아니라 Access-Control-Request-Method 헤더에 설정된 값을 그대로 Access-Control-Allow-Methods 에 설정하고 Access-Control-Request-Headers 헤더에 설정된 값을 그대로 Access-Control-Allow-Headers 헤더에 설정하기 때문이다.
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowedOriginPatterns(List.of("*"));
corsConfiguration.setAllowedHeaders(List.of("*"));
corsConfiguration.setAllowedMethods(List.of("*"));
corsConfiguration.setMaxAge(86400L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}