CORS는 브라우저가 현재 도메인 밖의 다른 도메인에 속한 자원과 상호작용하기 위해 사용하는 HTTP 헤더 기반의 메커니즘이다. 출처가 다른 어플리케이션 간 상호작용은 CSRF 등의 보안 문제에 취약하기 때문에, 웹은 같은 출처에서만 자원을 공유케 하는 Same-Origin Policy를 수립하였다. 그러나 웹 어플리케이션에서 다른 출처의 자원(서드 파티 api, 라이브러리 등)을 가져와 로드하는 일이 매우 빈번하기 때문에 SOP에 예외 조항을 두었고, 이에 따라 CORS 정책에 부합한다면 현재 도메인 밖의 출처로 자원을 요청할 수 있게 되었다. CORS 정책 부합 여부는 서버가 아닌 브라우저가 판단한다.
출처(Origin)는 위 그림의 Protocol, Host 그리고 생략된 포트 번호를 합친 것으로, 서버의 위치를 찾아갈 때 필수적인 정보의 합을 일컫는다. 따라서 같은 출처라 함은 Protocol/Scheme, Host, Port Number 값이 동일한 출처를 말한다.
URL | 구분 | 이유 |
---|---|---|
http://store.aws.com/dir/page.html | 클라이언트 URL | |
http://store.aws.com/dir2/new.html | 동일한 오리진 | 경로만 다름 |
http://store.aws.com/dir/inner/other.html | 동일한 오리진 | 경로만 다름 |
https://store.aws.com/page.html | 다른 오리진 | 다른 프로토콜 |
http://store.aws.com:81/dir/page.html | 다른 오리진 | 다른 포트(http:// 는 기본적으로 포트 80임) |
http://news.aws.com/dir/page.html | 다른 오리진 | 다른 호스트 |
Simple Request
GET
, POST
메서드 사용 시에만 사용 가능한 방식이다. 또한, 해당 방식과 사용 가능한 헤더의 종류에도 제한이 있고, Content-Type
을 사용하는 경우 multipart/form-data
, text/plain
등만 한정적으로 허용한다. 따라서 사용 빈도가 높은 text/xml
또는 application/json
컨텐츠 타입에 대해서는 사용이 불가하다.
해당 방식 사용 시 예비 요청(Preflight request) 없이 바로 서버에게 본 요청을 보내고, 서버가 응답 헤더에 Access-Control-Allow-Origin
값을 보내주면 이를 바탕으로 브라우저가 CORS 정책 위반 여부를 검사한다. 위의 이미지에서 보면 Access-Control-Allow-Origin
의 값이 *인데, 이는 어떠한 출처에서든 해당 자원을 액세스할 수 있음을 의미한다.
Preflight Request
일반적인 웹 어플리케이션에서 대부분 사용하는 방식이다. 자원 요청 시 브라우저가 서버에 예비 요청을 먼저 전송하고, 서버의 응답에 따라 같은 서버로 본 요청을 보낸다.
**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: X-PINGOTHER, Content-Type**
예비 요청은 OPTIONS
라는 HTTP/1.1 메서드를 사용하여 이루어진다. Origin
헤더에는 자신의 도메인을, Access-Control-Request-Headers
에는 본 요청에서 사용할 헤더 정보들을 넣어 예비 요청을 전송한다.
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
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
예비 요청을 받은 서버는 자원 액세스 가능 여부를 판단하여 브라우저에 응답을 전송한다. 응답 헤더들을 살펴보면 어떠한 출처에서 자원을 액세스 할 수 있는지 뿐만 아니라 어떠한 HTTP 메서드로 자원에 접근 가능한지, 예비 요청에서 브라우저가 보낸 Access-Control-Request-Headers
의 헤더들을 본 요청에서 사용 가능한지 여부 등을 명시하고 있다. 한편, 예비 요청은 서버의 응답에 명시된 시간만큼 캐싱될 수 있다.
POST /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
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache
예비 요청에서 자원 액세스가 가능하다는 응답을 받았기 때문에 브라우저는 서버로 본 요청을 전송한다. 본 요청의 내용은 Simple Request와 동일하다.
CORS 정책 위반 시에도 보안 상의 이유로 에러의 상세 내용은 확인할 수 없으므로 처음부터 에러가 발생하지 않도록 방지하는 것이 중요하다. CORS 정책 위반 여부는 브라우저에서 검사하지만 방지 조치는 주로 서버단에서 수행한다.
커스텀 필터를 생성하거나 WebMvcConfigurer를 이용해서 처리할 수도 있지만 내가 사용한 방법은 컨트롤러에서 @CrossOrigin
어노테이션을 추가해주는 것이었다. 서드 파티 api를 사용한 부분에서 계속 CORS 에러가 났는데 해당 api를 호출하는 컨트롤러 메서드에 해당 어노테이션을 추가해주니 에러가 해결되었다. @CrossOrigin
은 컨트롤러 레벨이나 메서드 레벨 중 선택해서 추가하거나 둘 다 해주어도 된다.
@Controller
@CrossOrigin(origins = "https://foo.example") // 컨트롤러에서 설정
public class AdminController{
@GetMapping
@CrossOrigin(origins = "https://foo.example") // 메서드에서 설정
public String memberList(){
}
}
* 해당 글은 다음의 글들을 참고하여 작성하였습니다:
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
https://evan-moon.github.io/2020/05/21/about-cors/
https://aws.amazon.com/ko/what-is/cross-origin-resource-sharing/
https://wonit.tistory.com/572