우리는 CORS 에러가 나타나면 머리가 지끈지끈 합니다. 저는 CORS를 '에러'의 한 종류라고 생각했어요. 그런데 CORS는 에러가 아니라 '정책'이라는 사실 아셨나요? 오늘 이 글을 읽고 여러분이 CORS에 대한 오해를 풀 수 있었으면 좋겠습니다. 자신도 모르게 많은 미움을 받았을 CORS에게 이 글을 바칩니다!
Porpose CORS를 확실히 이해한다.
CORS
CORS는 Cross-Origin Resource Sharing의 약자로 W3C에서 내놓은 정책입니다.
W3C (World Wide Web Consortium)
W3C 의 규약 중 CORS 관련 규약이 존재합니다. 이것을 각 브라우저 벤더들(크롬을 만드는 구글, IE를 만드는 마이크로소프트, 파이어폭스를 만드는 모질라, 사파리를 만드는 애플 etc...)이 읽어보고 규약에 맞추어 브라우저를 만들게 됩니다.
Cross-Origin Resource Sharing는 Cross-Origin(다른 출처)의 Resrouce를 공유하는 정책이라고 해석할 수 있습니다. 우리가 마주친 거의 모든 CORS 관련 이슈는 이런 CORS 정책 위반에서 비롯되는 것입니다.
이렇게 CORS가 정책이라는 것을 알았어도 밉게 느껴질 수 있습니다. 그런데 사실 CORS는 개발자들을 편하게 해주기 위해 등장한 정책이에요.
CORS가 등장하기 전
SOP를 아시나요? SOP는 Same-Origin Policy의 약자로 같은 출처에서만 리소스를 공유할 수 있다는 규칙입니다. 2011년 등장한 보안 정책으로 동일한 출처 사이에서만 리소스를 공유할 수 있다는 규칙을 가지고 있었습니다.
1) 브라우저에서 다른 서버에 요청할 경우에 해당되고, 브라우저를 거치지 않고 서버간 통신을 할 때는 이 정책이 적용되지 않습니다.
2) SOP는 자바스크립트 엔진 표준 스팩에 존재하는 보안 규칙입니다. 그렇기때문에 <script></script> 코드가 아닌 다른 파일들은 가져올 수 있습니다. js 표준 스팩에 존재하기때문에 js 파일이 아닌 css 파일 등은 가져올 수 있는 것입니다.
사실 출처가 다른 두 개의 어플리케이션이 마음대로 소통할 수 있는 환경은 꽤 위험한 환경입니다. CSRF Cross-Site Request Forgery 공격이 있습니다. CSRF 공격은 사용자의 권한을 도용해 중요 기능을 실행하는 공격입니다.
사용자가 웹사이트에 접근할 때는 브라우저의 쿠키에 토큰을 남깁니다. 해당 토큰을 사용하면 매번 로그인 할 필요 없이 서비스를 이용할 수 있게 됩니다. 그러나 로그인 토큰이 남아있는 브라우저에서 사용자가 악성 사이트에 접속한다면 악성 사이트 역시 사용자 브라우저의 토큰에 접근할 수 있게 됩니다.
만약 SOP가 없었다면 악성 사이트는 해당 토큰으로 나쁜 요청들을 마구 보낼 수 있었을 것입니다. SOP로 인해 악성 사이트의 요청 서버와, 응답을 주는 응답 서버의 출처가 다른 걸 보고 요청을 막을 수 있는 것입니다. (동일 출처가 아닌 경우 브라우저는 접근 차단)
그러나 다른 출처에 있는 리소스를 가져와서 사용하는 일은 굉장히 흔한 일이라 무작정 막을 수 없습니다.
웹 서비스가 다양해지면서 여러 서비스간 보다 자유롭게 데이터를 주고받을 필요가 생겼기 때문입니다. 그런데 다른 사이트간의 요청을 브라우저가 막고 있으니까, 개발자들은 Jsonp 등의 방식으로 이를 우회해서 사용하기 시작했습니다.
이걸 합의된 출처들간에 '합법적'으로 허용해주기 위해 기준을 충족시키면 리소스 공유가 되도록 만들어진 매커니즘이 CORS, 교차 출처 자원 공유 방식입니다. CORS 정책을 지키면 SOP의 제약을 받지 않게 되는 것입니다. 만약 정책을 지키지 않고 다른 출처간 요청을 보내면 우리가 아는 CORS(를 사용하라는) 에러가 내려지는 것이죠.
SOP 👉 하지만 다른 사이트 간의 요청이 필요 👉 개발자들이 Jsonp로 우회 👉 합법적으로 해라 👉 SOP의 예외 정책, CORS
이처럼 CORS가 등장하기 전에는 다른 출처 리소스를 사용할 수도 없었습니다.
원래는 동일 출처(Same-Origin)에서밖에 공유가 안 되었는데, CORS 정책으로 다른 출처(Cross-Origin)에서도 공유가 가능해졌습니다. 그러면 다르고, 같다를 구분하는 기준은 어떻게 될까요?
Origin(출처)에는 ① scheme, ② host, ③ port 가 있습니다.
그래서 same-origin이란 scheme(프로토콜), host(도메인), 포트가 같다는 말이며, 이 3가지 중 하나라도 다르면 cross-origin이 됩니다.
여기서 중요한 사실 한 가지는 앞에서 언급했듯 이렇게 출처를 비교하는 로직이 서버에 구현된 스펙이 아니라 브라우저에 구현되어 있는 스펙이라는 것입니다.
만약 우리가 CORS 정책을 위반하는 요청을 하더라도 해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아닌 이상 서버는 정상적인 응답을 한다는 것입니다. 단, 이후 브라우저가 이 응답을 분석해서 CORS 정책 위반이라고 판단되면 그 응답을 사용하지 않고 그냥 버릴 뿐이죠.
1) CORS는 브라우저 구현 스펙에 포함되는 정책이기때문에 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않습니다. 2) 또한 CORS 정책을 위반하는 리소스 요청때문에 에러가 발생했다고 해도 서버 쪽 로그에는 정상적으로 응답했다는 로그만 남기 때문에, CORS가 돌아가는 방식을 정확히 모르면 에러 트레이킹에 난항을 겪을 수도 있습니다.
서버가 아니라 브라우저에서 CORS 에러를 낸다는 점을, 출처를 브라우저에서 비교한다는 점을 기억하자.
브라우저는 다른 출처(cross-orgin) 요청이 보내질 때 origin이라는 header을 추가합니다. origin 항목에는 내 사이트의 프로토콜, 도메인, 포트가 담깁니다.
요청을 받는 api (ex 네이버 지도 api)는 지정된 access-control-allow-origin 정보를 실어 보냅니다. 만약 내 사이트가 등록된 상태면 (허용된 상태면) 내 사이트의 url도 access-control-allow-origin 정보에 들어있습니다.
브라우저는 이 둘을 비교합니다. origin에서 보낸 출처값이 서버의 답장에 담긴 access-control-allow-origin에 똑같이 있으면 안전한 요청으로 간주하고 응답 데이터를 받아오게됩니다.
브라우저는 요청을 한번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송합니다.
이 때 브라우저가 본 요청을 보내기 전에 보내는 예비 요청을 Preflight라고 부릅니다. 예비 요청에는 HTTP 메서드 중 OPTIONS 메서드가 사용됩니다. 예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것입니다.
인증된 요청을 사용하는 방법입니다. CORS의 기본적인 방식이라기 보다는 다른 출처 간 통신에서 보안을 좀 더 강화하고 싶을 때 사용하는 방법입니다.
요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 credentials옵션입니다. 옵션에는 총 3가지 값을 사용할 수 있습니다. ① same-origin(같은 출처 간 요청에만 인증 정보 담음)이 기본값이고 ②include(모든 요청에 인증 정보 담음)와 ③omit(모든 요청에 인증 정보 담지 않음)이 있습니다.
브라우저는 Access-Control-Allow-Origin: *라는 응답을 보고 괜찮다, 결론 내리지만 credentials 옵션을 include로 변경하고 같은 요청을 보내면 상황이 달라집니다. 인증 모드가 include일 경우 모든 요청을 허용한다는 의미의 *를 Access-Control-Allow-Origin 헤더에 사용해선 안 됩니다.
Spring Boot에서 CORS를 해결하는 방법
응답 헤더 Access-Control-Allow-Origin에 허용하고자 하는 도메인을 설정해줍니다. *는 모든 도메인을 허용합니다.
1) Config 클래스를 만들어주는 방법
@Configuration
public class WebConfig implements WebMvcConfigurer{
@Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET","POST");
}
}
1. @Configuration 어노테이션을 통해 설정 파일이라는 것을 알려줍니다.
2. WebMvcConfigurer을 구현 후 addCorsMappings(...) 메서드를 오버라이드합니다.
3. registry.addMapping을 이용해서 자원 공유를 허용할 url 패턴을 정의합니다. "/**"은 와일드카드로 모든 uri를 뜻합니다.
4. allowedOrigins() 메소드를 이용해서 자원 공유를 허락할 Origin을 지정할 수 있습니다.
.allowedOrigins("http://localhost:8080", "http://localhost:7007");
이처럼 한번에 여러 Origin을 설정할 수 있습니다.
5. allowedMethods()로 자원 공유를 허용할 HTTP Mehotd를 지정할 수 있습니다. 마찬가지로 "*"를 이용해 모든 method를 허용할 수 있습니다.
2) Annotation 이용하는 방법
@RequestMapping("/")
@CrossOrigin(origins="*", allowedHeaders="*")
public class AController{
}
@CrossOrigin 애노테이션을 사용하면 configuration같이 허용할 origin이나 methods를 지정할 수 있습니다. origins, methods, maxAge, allowedHeadrs 속성이 있고, 클래스가 아닌 메서드에 적용되게 할 수도 있습니다.
마무리하며
CORS는 단순한 에러가 아닌 우리를 편하게 해주는 정책입니다. 이를 잘 인지한다면 더이상 CORS에러에 머리싸매지 않아도 될것입니다. 오늘도 끝까지 읽어주셔서 감사합니다.
✅ 본 글은 2년전 작성한 글을 다시 재구성해 작성한 글입니다.
'Etc' 카테고리의 다른 글
엑셀 읽어오기 API 개발 시 꼭 고려해야하는 상황 및 예외처리 w 토지조서 엑셀, Apache POI 타입, 엑셀 서식 문제 (0) | 2025.03.02 |
---|---|
나는 어떻게 살아왔는가 - 삶의 지도 (0) | 2024.09.21 |