CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 브라우저가 자신이랑 같은 출처(Same Origin)가 아닌 다른 출처로부터 자원(Resource)을 요청하는 것을 허용하도록 서버가 허가해 주는 것이다. 그렇다면 출처(Origin)는 무엇을 기준으로 판단하는가?
우리는 아래와 같은 URL주소를 가지고 인터넷 사이트에 접속하게 된다. 이 URL을 분해해서 출처가 무엇인지 파악해 보겠다.
https://www.example.com:5000/post?query=title&page=1#subjet
URL은 위에서 분해한 것처럼 다양한 속성들로 구성이 되는데 출처(Origin)는 Protocal, Host, Port를 합친 것을 의미한다.
SOP는 웹 브라우저가 다른 출처에서 오는 요청을 제한하는 보안 매커니즘이다. 다른 출처에서 오는 요청을 허용하는 CORS랑은 반대된다. (여기서 출처를 비교하고 차단하는 작업은 서버가 아니라 브라우저가 한다) 이를 통해 XSS(Cross-Site Scripting, 크로스사이트 스크립팅)과 CSRF(Cross-Site Request Forgery, 크로스사이트 요청 위조)와 같은 보안 취약점을 방지할 수 있다.
하지만 SOP를 따라서 다른 출처에서 오는 요청을 전부차단하면 인터넷이 작동이 되지 않을 것이다.
그래서 우리는 SOP를 우회해서 다른 출처로부터 리소스를 가져올 수 있게 해야한다. 이때 필요한 것이 CORS다. 개발 환경에서는 중간 서버 역할을 하는 프록시 서버를 사용하여서 CORS에러를 우회할 수도 있다. webpack-dev-server나 Create React App에서 프록시 설정을 하면 된다.
<link rel="stylesheet" href="…" />
<script src="…"></script>
<img src="…" />
각 정책에 따라서 같은 도메인 서버에서 똑같은 이미지 파일을 불러와도 <img>태그의 src 속성을 사용할 때는 괜찮은데 자바스크립트로 ajax 요청으로 가져올 때는 CORS에러가 발생 할 수가 있다.
CORS에러는 브러우저의 SOP 정책을 위반하면서 CORS 정책을 따르지 않아서 발생한 에러이다. CORS 에러는 SOP정책을 위반하여도 CORS 정책을 따르면 해결이 된다. 즉, CORS에러를 해결하는 것은 SOP 파트에서 설명한 것처럼 SOP를 우회하는 것이다. 먼저 브라우저의 CORS 동작 과정을 살펴보겠다.
- 클라이언트(브라우저)에서 HTTP 요청 헤더에 Origin이라는 필에 출처를 담아서 서버에 요청을 보낸다.
- 서버는 응답 헤더에 Access-Control-Allow-Origin이라는 필드에 이미 지정된 허용할 url을 담아 클라이언트로 전달한다.
- 응답을 받은 클라이언트(브라우저)는 보낸 Origin과 서버로 부터 받은 Access-Control-Allow-Origin을 비교한다. 유효하면 리소스를 가져와서 쓰고, 유효하지 않으면 응답을 버리게 되고 CORS에러가 발생한다!
위 과정을 토대로 CORS에러는 서버에서 백엔드 개발자가 Access-Control-Allow-Origin헤더에 허용할 출처를 적용하면 해결이 된다는 것을 알아내었다! 아래에 Access-Control-Allow-Origin을 포함한 서버측에서 설정하는 헤더 종류가 있다.
Access-Control-Allow-Origin: * // 모든 출처 허용
Access-Control-Allow-Origin: https://example.com //이 출저만 허용
Access-Control-Allow-Methods: GET, POST, PUT // 요청을 허용할 HTTP 메서드를 설정 할 수 있다.
Access-Control-Allow-Headers: Content-Type, Authorization // 클라이언트가 요청에 포함할 할 수 있는 헤더를 지정한다.
브라우저가 본 요청을 보내기 전에 서버와 통신이 원활히 잘 되는지 확인하는 요청이다. 이 예비요청의 HTTP 메소드는 OPTIONS이다. 이 예비 요청은 보안을 강화하지만, 실제 요청에 걸리는 시간이 늘어나 성능에 악영향을 준다. 특히 API 요청이 많으면 예비 요청으로 인해 서버 요청이 2배가 되어 비용이 늘어난다. 이때 예비 요청을 브라우저 캐시를 이용해 Access-Control-Max-Age 헤더에 캐시될 시간을 지정하면 최적화를 할 수 있다.
예비 요청을 생략하고 바로 서버에 본 요청을 보낸 후 Access-Control-Allow-Origin로 CORS 위반을 확인 하는 방식이다. 단 예비요청을 생략할 수 있는 조건이 있다.
1. 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.
2. Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width 헤더여야 한다.
3. Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain중 하나여야한다.
대부분 HTTP API 요청은 text/xml 이나 application/json 으로 통신하기 때문에 예비 요청이 있다.
클라이언트(브라우저)가 요철할 때 서버에게 자격 인증 정보(Credential)을 실어 보내는 것이다.
자격 인증 정보는 세션 ID가 저장된 쿠키(Cookie) 혹은 Authorization 헤더에 설정하는 토큰 값 등이다. 아래 프로세스를 따른다.
Credential을 설정하지 않으면 브라우저가 제공하는 요청 API는 인증 데이터를 요청에 데이터에 담지 않는다. Credential 옵션은 3가지 값을 사용할 수 있다.
credentials 옵션 지정 예시 코드
// fetch 메서드
fetch("https://example.com:1234/users/login", {
method: "POST",
credentials: "include",
body: JSON.stringify({
userId: 1,
}),
// axios 라이브러리
axios.post('https://example.com:1234/users/login', {
profile: { username: username, password: password }
}, {
withCredentials: true
})
})