아이큐 두 자리도 이해하는 CORS(Cross-Origin Resource Sharing)

Cover Image for 아이큐 두 자리도 이해하는 CORS(Cross-Origin Resource Sharing)
swhan
· 5 min read

개발자들 귀찮게 만드는 CORS. 아이큐 두 자리도 이해할 수 있게 설명합니다.

한 줄 요약

“브라우저는 다른 출처의 자원을(Cross-Origin Resource) 사용하기 위해서(Sharing) 허락을 받아야 한다.”

기억할 것

  • 호출자: Access-Control-Request-*

  • 응답자: Access-Control-Allow-*

자원을 요청할 호출자는 Request, 자원을 가지고 있는 응답자는 Allow. 이 두 가지를 기억해두면 조금 더 쉽게 이해 가능합니다.

CORS(Cross-Origin Resource Sharing)

자본주의에서 가장 예민한 을 다루는 은행 사이트(bank.com)를 예시로 들어보겠습니다.

bank.com에 계좌를 가진 사용자(accountId=12345)가 친구에게(target) 송금하려고 할 때 송금 API가 아래처럼 작동한다고 가정해 보겠습니다.

POST /show-me-the-money

Host: bank.com
Content-Type: application/json
Cookie: accountId=12345

'{"target": "swhan", "money": "1,000,000,000"}';

만약 사용자가 실수로 해커 사이트(hacker-site.com)에 접속했을 때, 해커 사이트 측에서 심어놓은 스크립트가 작동해서 사용자 식별 정보(accountId=12345)를 가져다가 bank.com/show-me-the-money에 요청을 보내면 난리가 나겠죠?

보안을 위한 아주 간단한 방법은 외부 API 서비스를 없애버리면 됩니다. (???: 휴가 나간 군인이 사고를 친다고? 모든 군인의 휴가를 없애 버리면 무사고 가능한 거 아니야?)

하지만 은행 사이트가 아닌, 핀테크 서비스를 통한 송금 요청처럼 서로 다른 출처(Cross-Origin)끼리 통신이 필요한 시대에, 은행 입장에서는 무작정 다른 출처의 요청을 막아버릴 수는 없습니다.

콘텐츠를 가진 자가 왕인 세상인 만큼, 핀테크 서비스가 작동하는 브라우저는 송금 API를 호출하기 위해서(Resource Sharing) 은행 서버의 눈치를 보며 허락을 구해야 하는데요, 이렇게 다른 출처의 자원을 사용하기 위해서 허락을 구하는 방식을 표준화 시키기 위해 탄생한 것이 바로 CORS 정책입니다.

사전 요청(Preflighted Request)

CORS의 핵심은 사전 요청(Preflighted Request)이라고 볼 수 있는데요, 사전 요청은

  1. 실제 요청을 보내기 전에
  2. 브라우저가
  3. 다른 출처의 리소스에게
  4. OPTIONS 메서드로 HTTP 요청을 보내서
  5. 안전한 요청인지 판단한다

이렇게 정리할 수 있습니다. 핀테크 사이트에서(fintech-service.com) 은행(bank.com)의 송금 요청 API를 호출하는 코드를 보겠습니다.

fetch("https://bank.com/show-me-the-money", {
  method: "POST",
  mode: "cors", // CORS 모드라고 선언해주고
  credentials: "include", // 인증 정보(Cookie) 같이 보낼거라는 선언
  headers: {
    "Content-Type": "application/json",
  },
  body: '{"target": "swhan", "money": "1,000,000,000"}',
});

핀테크 사이트에서 이 코드가 실행되면

OPTIONS /show-me-the-money HTTP/1.1

Host: bank.com
Origin: https://fintech-service.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

브라우저는 메인 요청(POST)을 보내기 전에, 이렇게 OPTIONS 요청을 먼저 은행 서버에 보내서 메인 요청을 보내도 되는지 허락을 구하는데요, 이 요청의 의미는

  • Origin: fintech-service.com이라는 친구가 호출한 요청이고
  • Access-Control-Request-Method: 메인 요청에 사용할 method는 POST이고
  • Access-Control-Request-Headers: 메인 요청 헤더에는 content-type 값이 포함될 예정

이렇게 해석하시면 됩니다. 이제 은행 서버는 핀테크 서비스가 보낼 메인 요청을 받아주기로 했다면 아래처럼 응답을 돌려줍니다.

HTTP/1.1 204 No Content

Access-Control-Allow-Origin: https://fintech-service.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 60

이 요청의 의미는

  • Access-Control-Allow-Origin: fintech-service.com 이 친구는 우리 api 접근 가능하고
  • Access-Control-Allow-Methods: POST, GET, OPTIONS 요청만 허용할 거야.
  • Access-Control-Allow-Headers: 그리고 헤더 값으로 허용할 값든 Content-Type이고
  • Access-Control-Allow-Credentials: 쿠키, 인증 토큰 같은 자격 정보(credentials) 보내는 것도 허락할게
  • Access-Control-Max-Age: 그리고 60초 동안 인증 유효하니까 너네 브라우저에 캐시 해두고 재사용하렴

이렇게 해석 가능합니다. 이제 사전 요청으로 리소스 서버에게 허락을 받았으니 실제 요청(POST)을 전송할 수 있게 됩니다.

주의: Access-Control-Allow-Credentials: true를 사용하는 경우에는, Access-Control-Allow-Origin 값으로 와일드카드(*)는 절대 사용할 수 없습니다. 자격 정보(쿠키, 인증 토큰) 같이 예민한 정보를 보내는 만큼 요청을 허용할 명시적인 주소가 있어야 합니다

단순 요청(Simple requests)

직관적인 이해를 위해 당근 마켓을 예시로 들어보겠습니다. 당근 마켓에서 모르는 사람과 100만 원짜리 노트북 거래할 때와 대학교 캠퍼스 안에서 만 원짜리 중고 책 거래할 때 같은 수고(신원 확인, 접선 장소 조율, 상품 상태 확인)를 요구하지 않듯이, 송금 요청 API(POST)처럼 서버의 상태를 변경하는 게 아닌, 계좌 정보 조회 API(GET) 같은 단순 자원을 가져오는 요청은 브라우저 입장에서 안전한 요청이라고 볼 수 있습니다.

이러한 안전한 요청은 브라우저에서 단순 요청(Simple requests)으로 처리되는데요, 위에서처럼 사전 요청(Preflighted Request)을 통한 인증을 시도하지 않고, 바로 메인 요청을 시도합니다.

fintech-service.com에서 계좌 정보 가져오는 API를 호출하는 코드를 살펴보겠습니다.

fetch("https://bank.com/account-info", {
  method: "GET",
  mode: "cors",
  credentials: "include",
});

OPTIONS 메서드를 통한 사전 요청이 생략되고, 바로 GET 요청을 보내고

GET /account-info

Host: bank.com
Origin: https://fintech-service.com # 요청의 출처
Cookie: accountId=12345
HTTP/1.1 200 OK

Access-Control-Allow-Origin: https://fintech-service.com
Content-Type: application/json

'{ "result": "ok", "money": 10,000 }'

서버가 교차 출처 인증에서 가장 중요한 Access-Control-Allow-Origin 값을 응답 헤더에 담아서 보내주면 브라우저는 응답과 동시에 인증된 요청으로 처리합니다. (자원 요청과 인증을 한방에 해결)

주의: 본문에서는 단순 요청을 현대적인 웹 앱에서 자원을 가져올 때 가장 많이 사용되는 JSON API를 기준으로 설명했는데요, 실제 단순 요청으로 분류되는 특정 조건은 생각보다 더 까다롭습니다. 특정 조건(요청 방식, 헤더)이 뭐가 더 있는지 궁금하신 분들은 MDN 홈페이지에 있는 접근 제어 시나리오 예제-단순 요청(Simple requests) 항목을 참고해 주세요.

마무리

이렇게 CORS가 뭐고, 어떻게 작동하며, 무엇을 주의해야 하는지 알아보았는데요, 실제 웹 앱에 어떻게 적용되어서 사용되고 있는지 궁금하신 분들은 Cloudflare Workers & KV 이용해서 서버리스 방문자 카운팅 API 만들기 (2/2) 포스팅을 참고해 주시기 바랍니다.