이 친구는 뭐냐… 나는 완전히 처음 들어보는 친구다. 아니 친구가 맞나? 그냥 모르는 사이다.
처음부터 차근 차근 알아가보자.
교차 출처 리소스 공유(Cross-origin resource sharing, CORS), 교차 출처 자원 공유는 웹 페이지 상의 제한된 리소스를 최초 자원이 서비스된 도메인 밖의 다른 도메인으로부터 요청할 수 있게 허용하는 구조이다. 웹페이지는 교차 출처 이미지, 스타일시트, 스크립트, iframe, 동영상을 자유로이 임베드할 수 있다.
위키백과를 보니 이렇게 정리되어 있다. 간단히 하면 웹페이지 내에는 제한된 리소스만 있으니 외부 도메인에서 리소스를 추가로 가져올 수 있도록 하는 구조를 의미하는 것 같다.
→ 그럼 기존의 브라우저가 어떤 구조를 가지고 있기에 이런 구조가 나온 걸까?
브라우저는 기본적으로 SOP 정책을 따른다. SOP는 2011년 RFC 6454에서 나온 보안 정책으로
라는 규칙을 가지고 있다.
그러나 언제 개발이 내 맘대로 되었는가. 필연적으로 다른 곳에서 정보를 가져와야 하는 경우가 생길 수도 있으니 이런 경우를 대비해 몇 가지 예외를 두고 이 예외에 해당하는 경우에는 리소스를 가져다 쓸 수 있도록 했다.
이 예외 사항 중 하나가 바로 CORS 정책을 지킨 리소스 요청이다.
→ CORS라는 정책에 위배되지 않는다면 외부에서 리소스를 가져올 수 있게 해준다는 뜻이다.
당연하다면 당연한 거기도 하다. 바로 보안.
외부에서 정보를 가져오는 것은 꽤나 위험한 방법일 수 밖에 없다. 애초에 클라이언트와 서버를 이용하는 통신, 특히 웹을 이용하는 통신은 언제나 보안적 위험에 노출되어있음을 알고 있어야 한다. 당장 지금 화면에서 F11만 눌러보더라도 이 웹페이지에 떠 있는 정보를 모두 볼 수 있으니 말이다.
그러다 보니 다른 곳에서 정보를 가져오는 행위는 제한될 수 밖에 없었다.
→ 불편하지만 보안을 위해서 어쩔 수 없이 외부 데이터를 제한하지만 완전히 막으면 불편한 점이 반드시 생기므로 외부 데이터를 가져올 수 있는 조건을 명시해 해당 조건에 부합할 때만 데이터를 가져온다.
혹시 과거에 HTTP 프로토콜에 대해 이야기 했던 것을 기억하는가? 거기서 HTTP 통신을 위해 API를 보낼 때 Header에는 다양한 기본 정보들이 있는데 그 중 도메인에 대한 정보도 있다고 했다. SOP는 여기 있는 도메인을 이용해 기존 출처와 이외의 출처를 구분한다.
이후 필요 시 브라우저는 Header에 Origin이라는 필드를 이용해 데이터를 요청하는 출처를 함께 담아 보낸다.
서버는 이에 대한 응답을 할 때 리소스에 접근하는 것이 허용되었음을 Access-Control-Allow-Origin이라는 값으로 표시하고 브라우저는 이 표시와 기존의 요청을 비교해 유효한 응답인지 확인한다.
위와 같은 기본적인 흐름에서 크게 3가지의 시나리오가 있고 이에 따라 변경되는 부분이 있다.
Simple Request(단순 요청)
GET, HEAD, POST 요청만 가능
Accept, Accept-Language, Contet-Language, Content-Type과 같은 CORS 안전 리스트 헤더 혹은 User-Agent 헤더만 가능하다.
XMLHttpRequest 객체를 사용하여 요청하면, 요청에서 사용된 XMLHttpRequest.upload에 의해 반환되는 객체에 어떠한 이벤트 리스너도 등록되지 않는다.

그림에서 보듯 GET을 이용해 URL과 URI를 서버로 전송하며 데이터를 요청한다. 그리고 서버는 이에 대해 가져올 수 있는지 확인하고 Origin에서 허용되었음을 표시하며 답을 보낸다.
Preflight Request(사전 요청)
사실 사전 요청이 먼저 나왔어야 하나 싶기도 하다. HTTP 요청을 보낼 땐 보내기 전에 이 요청이 가능한지 먼저 확인을 해야 하는데 그 확인 과정에 쓰이는 것이 사전 요청이다.
OPTIONS 메서드로 HTTP 요청을 미리 보내 실제 요청이 전송하기에 안전한지 확인한다.
요청 헤더에는 다음 요소가 포함된다
응답 해더에는 아래 요소가 포함된다.
access-control-allow-origin : 서버가 허용하는 출처
access-control-allow-methods : 서버가 허용하는 HTTP 메서드 리스트
access-control-allow-headers : 서버가 허용하는 header 리스트
access-control-max-age : 프리 플라이트 요청의 응답을 캐시에 저장하는 시간

과정을 보면 이와 같다.
Credentialed Requests (자격 증명 요청)
자격 증명은 쿠키, 인증 헤더, TLS 클라이언트 인증서 등의 신용 정보와 함께 요청한다. 기본적으로 CORS 정책은 다른 출처 요청에 인증 정보 포함을 허용하지 않지만 요청에 인증을 포함하는 부분이 있거나 access-control-allow-credentials가 true로 설정 한다면 요청 가능하다.

하지만 이 요청 방법은 위에 적혀 있듯 기본적으로는 CORS에서 허용하지 않는 방식이기 때문에 사전 요청으로 확실히 확인을 하는 과정을 먼저 보고 자격 증명 요청이 꼭 필요한 경우에만 사용해야 한다. 보안적으로 인증서 등의 정보를 보내는 것은 옳지 못할 수 있기 때문이다.
CORS를 쭉 알아봤는데 위에서도 적었듯 외부에서 데이터를 요청하고 가져오는 것은 SOP에 위배되는 것이기 때문에 좋은 것은 아니다. 따라서 CORS에 문제가 생겼거나 혹은 CORS로도 하기 어려운 것들을 해결하는 법에는 어떤 것이 있는지 살펴본다.
Chrome 에서는 CORS 문제를 해결해주는 확장 프로그램을 제공하고 있다.
이 확장 프로그램을 활성화 하면 로컬 환경에서 API를 테스트 할 때 CORS 문제를 해결해 준다.
실제 배포 시엔 적용이 어렵지만 간단한 테스트에서 문제가 발생한다면 간편히 해결할 수 있는 좋은 수단이다.
Proxy란 클라이언트와 서버 사이의 중계 포인트이다. 프록시는 톰켓에서 서버에 대한 이야기를 할 때 언급했으니 참고 하면 될 것 같다.
결국 프록시는 서버들과 연결되어 우리에게 정보를 제공해주는 중간 서버 같은 것인데 이를 이용해 요청을 직접 서버에 보내는 것이 아니라 프록시에게 보내는 것이다.
다만 이 또한 악용 사례로 인해 api 요청 횟수를 제한해두었기에 실사용은 어렵고 테스트 시 사용하기 좋다.
const url = 'https://google.com' // 이 부분을 이용하는 서버 URL로 변경
fetch(`https://cors-anywhere.herokuapp.com/${url}`)
.then((response) => response.text())
.then((data) => console.log(data));
<script src='https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js'></script>
<script>
axios({
url: 'https://cors-proxy.org/api/',
method: 'get',
headers: {
'cors-proxy-url' : 'https://google.com/' // 이 부분을 이용하는 서버 URL로 변경
},
}).then((res) => {
console.log(res.data);
})
</script>
const url = 'https://google.com' // 이 부분을 이용하는 서버 URL로 변경
fetch(`https://proxy.cors.sh/${url}`)
.then((response) => response.text())
.then((data) => console.log(data));
.서버에서 Access-Control-Allow-Origin 헤더 세팅하기
HTTP 헤더
# 헤더에 작성된 출처만 브라우저가 리소스를 접근할 수 있도록 허용함.
# * 이면 모든 곳에 공개되어 있음을 의미한다.
Access-Control-Allow-Origin : https://naver.com
# 리소스 접근을 허용하는 HTTP 메서드를 지정해 주는 헤더
Access-Control-Request-Methods : GET, POST, PUT, DELETE
# 요청을 허용하는 해더.
Access-Control-Allow-Headers : Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization
# 클라이언트에서 preflight 의 요청 결과를 저장할 기간을 지정
# 60초 동안 preflight 요청을 캐시하는 설정으로, 첫 요청 이후 60초 동안은 OPTIONS 메소드를 사용하는 예비 요청을 보내지 않는다.
Access-Control-Max-Age : 60
# 클라이언트 요청이 쿠키를 통해서 자격 증명을 해야 하는 경우에 true.
# 자바스크립트 요청에서 credentials가 include일 때 요청에 대한 응답을 할 수 있는지를 나타낸다.
Access-Control-Allow-Credentials : true
# 기본적으로 브라우저에게 노출이 되지 않지만, 브라우저 측에서 접근할 수 있게 허용해주는 헤더를 지정
Access-Control-Expose-Headers : Content-Length
// 스프링 서버 전역적으로 CORS 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080", "http://localhost:8081") // 허용할 출처
.allowedMethods("GET", "POST") // 허용할 HTTP method
.allowCredentials(true) // 쿠키 인증 요청 허용
.maxAge(3000) // 원하는 시간만큼 pre-flight 리퀘스트를 캐싱
}
}
// 특정 컨트롤러에만 CORS 적용하고 싶을때.
@Controller
@CrossOrigin(origins = "*", methods = RequestMethod.GET)
public class customController {
// 특정 메소드에만 CORS 적용 가능
@GetMapping("/url")
@CrossOrigin(origins = "*", methods = RequestMethod.GET)
@ResponseBody
public List<Object> findAll(){
return service.getAll();
}
}앞서 설명한 우회 방법들은 Access-Control-Allow-Origin 세팅 이외에는 실전 사용이 어렵다. 이유는 당연하게도 보안 취약점 때문.. 안되는걸 예외로 억지로 열어줬으니 당연한 것이다. 그러니 신중히 사용해야 하는 것도 맞지만 내가 접근 하고자 하는 곳이 내가 원하는 데이터를 허용해 주지 않는 경우도 있다. 특히 아래와 같은 문제들은 추가적 공부로 극복 가능한 문제들이니 간략하게 짚어본다.

이런 메시지가 뜬다면 PNA 문제이다. 간단히 공인 네트워크의 웹사이트에서 사설 네트워크의 리소스를 호출할 때 에러가 뜨게 되는 문제이다. 서버는 당연하게도 공인되지 않은 사설 네트워크는 접근이 조심스러울 수 밖에 없기에 다른 에러가 아니라 Message의 형태로 에러를 띄워준다. 이런 문제는 결국 해당 사설망에 접근하는 지식을 알아야 한다.
캐시는 결국 브라우징을 하는 우리들에게 편리성을 부여해주는 아주 고마운 친구들이다. 하지만 캐시는 결국 과거의 데이터에 불과하므로 현재의 데이터와 불일치 하는 경우가 생길 수 있다. 이를 비롯한 자잘한 문제들 때문에 CORS에서 잘못되거나 어긋나는 정보로 인해 오류가 뜰 수도 있다.
CORS는 SOP라는 동일 출처의 데이터만 사용하는 정책을 지키면서도 필요시 외부 데이터를 가져오기 위한 하나의 수단으로 마련된 정책이다.
CORS는 다른 출처 사이에서 리소스의 공유가 가능하도록 해준다.