본문 바로가기

항해 99/Spring

CORS

CORS(Cross-Origin Resource Sharing)

교차 출저 리소스 공유 정책, 교차 출처라는 것은 (엇갈린) 다른 출처를 의미

 

출처(Origin)

어떤 사이트를 접속할 때 인터넷 주소창에 URL이라는 문자열을 통해 접근, URL은 다음과 같은 구성 요소로 이루어짐

 

  • Protocol(Schema) : http, https
  • Host : 사이트 도메인
  • Port : 포트 번호
  • Path : 사이트 내부 경로
  • Query string : 요청의 key와 value값
  • Fragment : 해시 태그

출처(Origin)라는 것은 Protocol과 Host 그리고 Port까지 모두 합친 URL을 의미함

 

동일 출처 정책(SOP, Same-Origin Policy)

SOP 정책은 동일한 출처에 대한 정책을 말함, '동일한 출처에서만 리소스를 공유할 수 있다'라는 법률을 가짐

동일 출처(Same-Origin) 서버에 있는 리소스는 자유로이 가져올 수 있지만, 다른 출처(Cross-Origin) 서버에 있는 이미지나 유튜브 영상 같은 리소스는 상호작용이 불가능함

 

 

동일 출처 정책이 필요한 이유

동일 출처 정책이 없으면 해커가 CSRF(Cross-Site Request Forgery)나 XSS(Cross-Site Scripting) 등의 방법을 이용해서 어플리케이션에서 해커가 심어놓은 코드를 통해 개인정보를 가로챌 수 있음

 

1. 사용자가 악성 사이트에 접속한다

2. 해커가 몰래 심어놓은 악의적인 자바스크립트가 실행되어, 사용자가 모르는 사이에 어느 포털 사이트에 요청을 보낸다.

3. 포털 사이트에서 해당 브라우저의 쿠기를 이용하여 로그인 하거나 등 상호작용에 따른 개인 정보 응답 값을 받은 뒤, 사이트에서 해커 서버(hacker.example)로 재차 보냄

4. 이외에도 사용자가 접속중인 내부망의 아이피와 포트를 가져오거나, 해커가 사용자 브라우저를 프록시처럼 악용할 수도 있음

 

따라서 이러한 악의적인 경우를 방지하기 위해, SOP 정책으로 동일하지 않은 다른 출처의 스크립트가 실행되지 않도록 브라우저에서 사전에 방지하는 것임

 

출처(Origin)의 동일함은 두 URL의 구성 요소 중 Protocol(Schema), Host, Port이 3가지만 동일하다면 동일 출처로 판단한다.

 

출처 판단은 브라우저가 한다.

출처를 비교하는 로직은 서버에 구현 도니 스펙이 아닌 브라우저에 구현된 스펙임

 

브라우저에는 에러가 뜨지만, 정작 서버 쪽에는 정상적으로 응답을 했다고 하기 때문에 난항을 겪는 것

즉, 응답 데이터는 멀쩡하지만 브라우저 단에서 받을 수 없도록 차단을 한 것

 

요청 방식에 따른 CORS 발생여부

브라우저에서 요청 방식은 두 가지가 있음

  • img, video, script, link 태그 등
    • 기본적으로 Cross-Origin 정책을 지원함
  • XMLHttpRequest, Fetch API 스크립트
    • 기본적으로 Same-Origin 정책을 따른다

자바스크립트에서의 요청을 기본적으로 서로 다른 도메인에 대한 요청을 보안상 제한한다.

브라우저는 기본으로 하나의 서버만 연결이 허용되도록 설정되어 있기 때문(주로 자신의 서버)

 

CORS 기본 동작

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 다른 출처의 리소스 공유에 대한 허용/비허용 정책

보안이 중요하지만, 개발을 하다보면 기능상 어쩔 수 없이 다른 출처 간의 상호작용을 해야 하는 케이스도 있으며, 또한 실무적으로 다른 회사의 서버 API를 이용해야 하는 상황도 존재함, 이와 같은 예외 사항을 두기 위해 CORS 정책을 허용하는 리소스에 한해 다른 출처라도 받아들인다는 것.

 

CORS는 다른 출처의 리소스를 얻기 위한 해결 방안, SOP 정책을 위반해도 CORS 정책에 따르면 다른 출처의 리소스라도 허용한다는 뜻

 

브라우저의 CORS 기본 동작

  1. 클라이언트에서 HTTP 요청의 헤더에 Origin을 담아 전달
  2. 서버는 응답헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달
  3. 클라이언트에서 Origin과 서버가 보내준 Access-Control-Allow-Origin을 비교

서버에서 Access-Control-Allow-Origin 헤더에 허용할 출처를 기재해서 클라이언트에 응답하면 되는 것(백엔드 개발자가 고쳐야될 부분)

 

Access-Control-Allow-Origin?

CORS에서 사용되는 HTTP 응답 헤더 중 하나로, 다른 도메인에서 요청을 허용하는 경우 어떤 도메인에서 요청을 허용할 것인지를 명시

  • 서버 A에서 자원을 제공하는 도메인이 www.example.com이고 다른 도메인인 www.otherdomain.com에서 이 자원에 접근하려는 경우 www.example.com 서버에서 Access-Control-Allow-Origin 헤더에 www.otherdomain.com를 명시에 요청을 허용할 수 있음
// 예시 코드
const http = require('http');

const server = http.createServer((req, res) => {
  // 모든 도메인에서의 요청을 허용하기 위해 Access-Control-Allow-Origin 헤더에 *을 설정합니다.
  res.setHeader('Access-Control-Allow-Origin', '*');

  // 서버로부터 받은 요청에 대한 응답을 작성합니다.
  res.write('Hello World!');
  res.end();
});

server.listen(3000, () => {
  console.log('Server listening on port 3000');
});

 

CORS 작동 방식 3가지 시나리오

실제 CORS가 동작하는 방식은 한 가지가 아니라 세 가지의 시나리오에 따라 변경된다.

 

예비 요청(Preflight Request)

브라우저는 요청을 보낼 때 한 번에 바로 보내지 않고, 먼저 예비 요청을 보내 서버와 잘 통신되는지 확인한 후 본 요청을 보낸다.

즉, 에비 요청의 역할은 본 요청을 보내기 전 브라우저 스스로 안전한 요청인지 미리 확인하는 것임

브라우저가 예비요청을 보내는 것을 Preflight라고 부르며, 에비요청의 HTTP 메서드는 GET이나 POST가 아닌 OPTIONS라는 요청이 사용된다는 것이 특징

 

  1. 자바스크립트의 fetch() 메서드를 통해 리소스를 받아오려고 한다
  2. 브라우저는 서버로 HTTP OPTIONS 메서드로 예비 요청(Preflight)을 먼저 보낸다
    1. Origin 헤더에 자신의 출처를 넣는다
    2. Access-Control-Request-Method 헤더에 실제 요청에 사용할 메서드를 설정한다
    3. Access-Control-Request-Headers 헤더에 실제 요청에 사용할 헤더들을 설정한다
  3. 서버는 이 요청에 대한 응답으로 어떤 것을 허용하고 어떤 것을 금지하고 있는지에 대한 헤더 정보를 담아서 브라우저로 보내준다
    1. Access-Control-Allow-Origin 헤더에 허용되는 Origin 들의 목록을 설정한다
    2. Access-Control-Allow-Methods 헤더에 허용되는 메서드들의 목록을 설정한다
    3. Access-Control-Allow-Headers 헤더에 허용되는 헤더들의 목록을 설정한다
    4. Access-Control-Max-Age 헤더에 해당 예비 요청이 브라우저에 캐시될 수 있는 시간을 초 단위로 설정한다
  4. 이후 브라우저는 보낸 요청과 서버가 응답해준 정책을 비교하여, 해당 요청이 안전한지 확인하고 본 요청을 보내게 된다
  5. 서버가 본 요청에 대한 응답을 하면 최종적으로 이 응답 데이터를 자바스크립트로 넘겨준다

예비 요청의 문제점과 캐싱

예비 요청을 보내기 전에 OPTIONS 메서드로 예비 요청을 보내 보안을 강화하는 목적의 취지는 좋으나 실제 요청에 걸리는 시간이 늘어나게 되어 어플리케이션 성능에 영향을 미치는 크나 큰 단점이 있음

 

수행하는 API 호출 수가 많으면 많을 수록 예비 요청으로 인해 서버 요청을 배로 보내게 되어 비용적인 측면에서 폐가될 수 있음, 따라서 브라우저 캐시(Cathe)를 이용해 Access-Cotrol-Max-Age 헤더에 캐시될 시간을 명시해 주면, 이 Preflight 요청을 캐싱시켜 최적화할 수 있음

 

예비 요청 캐시는 다른 캐싱 매커니즘과 유사하게 작동

 

  1. 브라우저는 예비(Preflight) 요청을 할 때마다, 먼저 Preflight 캐시를 확인하여 해당 요청에 대한 응답이 있는지 확인한다.
  2. 만일 응답이 캐싱 되어 있지 않다면,  서버에 예비 요청을 보내 인증 절차를 밟는다.
  3. 만일 서버로 부터 Access-Control-Max-Age 응답 헤더를 받는다면 그 기간 동안 브라우저 캐시에 결과를 저장한다.
  4. 다시 요청을 보내고 만일 응답이 캐싱 되어 있다면, 예비 요청을 서버로 보내지 않고 대신 캐시된 응답을 사용한다.

 

단순 요청(Simple Request)

단순 요청은 말그대로 예비 요청(Preflight)을 생략하고 바로 서버에 직행으로 본 요청을 보낸 후, 서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin 헤더를 보내주면 브라우저가 CORS정책 위반 여부를 검사하는 방식

 

심플한 만큼 특정 조건을 만족하는 경우에만 예비 요청을 생략할 수 있음

  • 요청의 메서드는 GET, HEAD, POST 중 하나여야 한다
  • Accept, Accept-Language, Content-Language, Content-Tupe, DPR, Downlink, Save-Data, Viewport-Width, Width 헤더일 경우에만 적용됨
  • Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야 함, 아닐 경우 예비 요청으로 동작됨

다소 까다로운 조건들이 많기 때문에, 위 조건을 모두 만족하여 단순 요청이 일어나는 상황은 드물다

대부분 HTTP API 요청은 text/xml 이나 application/json 으로 통신하기 때문에 Content-Type이 위반되기 때문에 예비 요청(preflight)로 이루어진다

 

인증된 요청(Credetialed Request)

인증된 요청은 클라이언트에서 서버에게 자격 인증 정보(Credential)를 실어 요청할 때 사용되는 요청(자격 인증 정보란 세션 ID가 저장되어 있는 쿠키(Cookie) 혹은 Authorization 헤더에 설정하는 토큰 값 등)

 

클라이언트에서 인증 정보를 보내도록 설정

기본적으로 브라우저가 제공하는 요청 APIe들은 별도의 옵션 없이 브라우저의 쿠키와 같은 인증 데이터를 함부로 요청 데이터에 담지 않도록 되어 있음

이때  요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 credential이다. 이 옵션에는 3가지 값을 사용할 수 있다.

  • same-origin(기본값) : 같은 출처 간 요청에만 인증 정보를 담을 수 있다
  • include : 모든 요청에 인증 정보를 담을 수 있다
  • omit : 모든 요청에 인증 정보를 담지 않는다

별도의 설정을 해주지 않으면 쿠키 등의 인증 정보는 절대로 자동으로 서버에게 전송되지 않는다

 

서버에서 인증된 요청에 대한 헤더 설정하기

서버도 마찬가지로 인증된 요청에 대해 일반적인 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 정책에 의해 응답이 거부 됨(인증 정보는 민감한 정보이기 때문에 출처를 정확하게 설정해주어야 함)

 

CORS 3가지 시나리오 작동 테스트 사이트 CORS-Tutorial

 

 

참조

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F