CORS의 개념과 해결법

Sheryl Yun·2021년 12월 31일
8

출처: [10분 테코톡 - 나봄의 CORS] https://www.youtube.com/watch?v=-2TgkKYmJt4&list=RDCMUC-mOekGSesms0agFntnQang&index=9


프론트엔드와 백엔드 협업 시
다음과 같은 CORS 에러 메시지를 본 적이 있을 것이다.

CORS를 이해하기에 앞서, SOP라는 개념에 대해 먼저 알아보자.

SOP (Same Origin Policy)

SOP란, '다른 출처의 리소스 사용을 제한하는 보안 정책' 이다.

여기서 '출처'라는 건 무엇일까?

출처(Origin)

출처가 같은지 다른지 여부URL의 구성으로 판단이 가능하다.

다음 그림은 URL의 예시이다.

URL의 맨 앞 3개의 요소(프로토콜, 호스트, 포트번호)를 통해
같은 출처인지 다른 출처인지 판단한다.

반드시 세 요소가 모두 같아야만 같은 출처이고,
하나라도 다르면 다른 출처이다.

예외
IE(인터넷 익스플로러)의 경우, Port를 판단 기준에서 제외
(= Port가 달라도 같은 출처라고 판단)


Q. 퀴즈 -- 출처 구분하기

다음 중 'http://localhost' 와 같은 출처인 url은?

1번. https://localhost
2번. http://localhost:80
3번. http://127.0.0.1
4번. http://localhost/api/cors

.
.
.
.

정답은 2번과 4번이다.

1번은 'http'가 아닌 'https'이므로 프로토콜이 다르다.

2번은 http 프로토콜에 80 포트가 붙여져 있는 형태인데
http의 기본 포트가 80 포트이므로 생략하면 동일하다.

3번은 127.0.0.1의 IP가 localhost임에도 불구하고,
브라우저 입장에서는 URL을 '문자열값'으로 비교하기 때문에 다르다고 판단한다.

4번은 /api/cors 부분이 추가적으로 붙는 location(로케이션)인데,
로케이션은 말 그대로 부사 같은 '추가적인 정보'일 뿐이어서 /api 앞에까지만 비교하면 동일하다.



다시 SOP로 돌아가자.

아까 SOP는 '보안 정책'이라고 했다.
그러면 다른 출처의 리소스 사용을 제한하는 것이 어째서 보안에 도움이 되는 걸까?
다음 예시를 살펴보자.

예시

  1. 사용자가 페이스북을 이용하기 위해 로그인을 하면 입력 정보가 페이스북 서버로 넘어간다.

  2. 페이스북 서버는 사용자의 로그인 정보를 확인한 뒤 인증 토큰을 발급한다.

  3. 인증 토큰이 발급된 상황에서, 해커가 페이스북에 이상한 게시글을 등록하는 스크립트가 담긴 메일을 사용자에게 보낸다.

  4. 사용자가 해당 메일을 클릭하면 해커가 만든 주소('http://hacker.ck')로 이동하고, 사용자의 인증 토큰을 통해 페이스북 사용자의 계정에 접근된다.

=> 여기서 SOP 정책의 존재 여부에 따라 두 가지 상황으로 나뉘게 된다.

SOP 정책이 없는 경우

페이스북이 단순히 인증 토큰 여부만 보고 접근을 허가하여
사용자의 계정에 해커가 요청한 이상한 내용이 실행된다.

SOP 정책이 있는 경우

페이스북이 사용자의 인증 토큰을 받은 후에
사용자의 요청이 어디서 온건지 출처(Origin) 확인을 한다.

이때 페이스북이 확인한 출처(http://hacker.ck)가
원래 받아야 할 출처(페이스북 로그인 페이지, https://www.facebook.com/login)와 다르면
출처가 다르다고(cross-origin) 판단하여 사용자가 보낸 이상한 스크립트 요청을 받아들이지 않게 된다.

결론

즉, SOP 정책이란
보안을 위해 다른 출처의 리소스 사용을 제한(금지)하는 것이다.

그리고 이렇게 이상한 게시글을 올리는 정도가 아니라 송금과 같은 민감한 개인정보들이 공유되면 치명적인 보안 문제가 발생할 수 있다.

CORS (Cross-Origin Resource Sharing)

위의 해커 예시와 같은 상황이 발생할 수 있음에도 불구하고,
사용자가 웹 서비스를 이용하다 보면 다른 출처의 리소스가 필요한 경우가 생긴다.

CORS는 영어를 그대로 번역하면 '교차 출처 리소스 공유'이다.
교차 출처, 즉 서로 다른 출처의 자원을 공유할 수 있게 하는 것이다.

CORS는
추가 HTTP 헤더를 사용하여,
한 출처의 자원이 다른 출처의 자원에게 접근 가능하도록
브라우저에게 알려주는 것이다.
(출처: Mozilla 사이트)

추가 HTTP 헤더 사용 부분은 아래에서 좀 더 살펴보자.

CORS 접근제어 시나리오

CORS 상황에서 브라우저와 서버가 통신하는 방식으로 다음처럼 3가지가 있다.

1) 단순 요청
2) Preflight 요청
3) 인증정보 포함 요청

1. 단순 요청 (Simple Request)

preflight 요청 없이 바로 본 요청을 보내고 cross-origin 여부를 확인하는 방식이다.

위의 그림에서 Client의 출처(Origin)는 foo.example인 반면,
Server는 모든 출처(*)의 요청을 허용하고 있다.

CORS가 발생할 수 있는 상황에서도 아무런 장치 없이 본 요청을 보내는 것이다.

[ 단순 요청 시 만족해야 하는 조건]

  • GET, POST, HEAD 메서드만 가능
  • Content-Type은 다음 중 하나여야
    1) application/x-www-form-urlencoded
    2) multipart/form-data
    3) text/plain
  • Header도 다음 중 하나여야
    1) Accept
    2) Accept-Language
    3) Content-Language
    4) Content-Type

발생 가능한 문제

CORS 개념을 인식 못하는 서버들에게 이러한 단순 요청을 보낼 경우,
CORS 에러가 발생할 수 있다.

2. 프리플라이트 요청 (Preflight Request)

Preflight에서 pre는 '미리', flight는 '비행'을 뜻한다.
즉 '본격적인 일(비행)이 일어나기 전에 뭔가를 미리 한다'는 정도로 이해하면 되겠다.

Preflight가 생겨난 배경

CORS라는 개념이 생기기 이전의 서버들은
브라우저의 SOP가 가능하다는 전제 하에 만들어졌다.

하지만 CORS 개념이 등장하면서 cross-origin 요청이 가능해진 이후에는
이렇게 CORS와 같은 보안 장치가 없는 서버들을 보호할 필요가 생겨났다.

이렇게 교차 출처가 서로 달라 CORS 상황을 인식하지 못하는
서버들을 보호하기 위해 Preflight 개념이 등장하였다.

Preflight 요청 개념

본 요청이 가능한지 먼저 서버에게 사전 확인을 한 뒤
서버가 가능한 경우에만 요청을 하는 것이다.

Preflight 요청 절차

  1. 먼저 HTTP 요청 메시지에 있는 Request Method
    OPTIONS 메서드를 넣는다. (두번째 검은 박스 보기)

  2. 그러면 요청이 2번 가게 된다.

  • 1번째 : Preflight 사전 요청
    실제 요청을 '이러이러하게~' 보내도 되는지 서버에게 물어보는 것

    여기서 실패하면 브라우저는 405 Method Not Allowed 에러를 발생시키고, 실제 요청은 서버로 전송하지 않음

  • 2번째 : 실제 요청
    Preflight 사전 요청이 성공하면 실제 본 요청을 보내게 됨


  1. 서버에서도 요청 횟수에 맞게 응답을 2번 한다.
  • 1번째 : Preflight 사전 요청에 대한 응답
    본 요청을 (그렇게) 보내도 된다 vs. 안 된다 여부를 알려줌

  • 2번째 : 실제 요청에 대한 응답
    실제 요청에서 요구한 데이터를 전달


preflight 요청 내용과 포맷

  1. Origin: 이 요청이 어디에서 보내지는지 (브라우저쪽 출처)
  2. Access-Control-Request-Method: 어떤 메서드를 실제 요청에 써도 되는지 (GET, POST 등)
  3. Access-Control-Request-Header: 실제 요청 시 어떤 헤더를 추가로 보낼 건지 (Content-Type 등)

Preflight 응답 내용과 포맷

  1. Access-Control-Allow-Origin: 이 출처를 허가함
  2. Access-Control-Allow-Methods: 이 메서드들을 허가함
  3. Access-Control-Allow-Headers: 이 헤더들을 허가함
  4. Access-Control-Max-Age: preflight 응답 캐시가 유효한 시간

    Access-Control-Max-Age
    Preflight 방식은 매번 2번의 요청을 보내야 하는데
    이는 리소스 사용에 좋지 않다.
    따라서 '캐싱'을 하여, 첫 번째 요청에서 Preflight 요청이 통과되면
    캐싱이 유지되는 시간 동안에는 그 다음 Preflight 요청을 건너뛰고 바로 본 요청을 보낼 수 있도록 한다.
    (즉, Access-Control-Max-Age = 캐싱이 유지되는 시간)


3. 인증정보 포함 요청 (Credential Request)

인증 관련 헤더를 포함할 때 사용하는 요청 방식이다.

예: JWT 토큰을 클라이언트에서 자동으로 담아서 보내고 싶을 때

  1. 요청 메시지 헤더에 다음과 같이 작성한다.

Access-Control-Allow-Credentials: include

  1. 서버에서 위의 헤더를 받기 위한 옵션을 작성한다.
  1. Access-Control-Allow-Credentials: true
  2. Access-Control-Allow-Origin: "요청을 받을 구체적인 출처"
    (와일드카드 * 을 사용해서 모든 출처를 허용해버리면 에러가 발생할 수 있으므로 꼭 구체적인 출처를 명시한다)

CORS 해결하기

CORS 에러를 해결하기 위해서는 다음과 같은 방법이 있다.

1. 프론트엔드: Proxy 서버 설정

1) axios 사용 시 (한 줄로 해결 가능)

React를 개발할 때, 보통 dev server의 접속 주소는 http://localhost:3000 을 사용한다.
만약 api server의 주소가 http://apiserver.com:5000 이면
따로 CORS설정을 해놓지 않은 이상 api request를 날릴 시 CORS 오류가 발생한다.

해결방법

package.json에 proxy 한 줄을 추가한다.

//package.json
{
  ...,
  "proxy": "http://apiserver.com:5000",
  ...,
}

그러면 다음과 같이 request가 처리된다.

  1. browser에서 React dev server(http://localhost:3000)으로 요청을 보낸다.
  2. React dev server가 해당 요청을 api server(http://apiserver.com:5000)에 보낸다.
  3. api server가 react dev server에게 응답 내용을 전달한다.
  4. React dev server는 이 응답을 그대로 browser에게 전달한다.
  5. 이때 browser 입장에서는 api server가 아닌, React dev server가 응답한 것 처럼 보이게 된다.

2) axios 사용 시 또 다른 해결법 (백엔드 처리 필요)

  • 프론트엔드 작업
    axios 옵션에 withCredentials: true 추가
const handleLogin = () => {
  axios.post(`${EndPoint.APIServer}/myauth/login/`, {
    profile: {
      username: username,
      password: password
    }
  },
    { withCredentials: true }
  ).then(response => {
    console.log(response);
    console.log(response.data);
  })
}
  • 백엔드 작업
    settings 파일에 CORS_ALLOW_CREDENTIALS 설정을 true로 바꾼다.
// settings.py
CORS_ALLOW_CREDENTIALS = True

3) fetch의 경우

webpack-dev-server가 제공하는 proxy 기능을 사용한다.

=> 웹팩 설정 파일(예: vue.config.js)에 다음 내용을 추가

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.evan.com',
        changeOrigin: true,
        pathRewrite: { '/api': '/' },
      },
    }
  }
}

위의 코드를 설명하면,

로컬 환경에서 /api로 시작하는 URL로 보내는 요청에 대해
브라우저는 같은 출처인 localhost:8000/api 서버로 요청을 보낸 것으로 인식하지만,

뒤에서는 웹팩이 실제 서버인 https://api.evan.com으로
요청을 보내주기 때문에

서로 다른 출처에서 자원을 주고받음에도 불구하고
마치 SOP 정책을 지킨 것처럼 브라우저를 속일 수 있다.

2. 백엔드에서 해결하기

1) 서버에서 직접 헤더에 설정

Access-Control-Allow-Origin 항목에 요청받을 출처를 정확하게 명시한다.
(근데 매번 이렇게 해주는 건 귀찮아서 주로 스프링부트의 기능을 이용한다고 함)

2) SpringBoot의 기능 이용

@CrossOrigin 키워드를 통해 origin을 구체적으로 명시한다.
(와일드카드 * 를 쓰면 오류가 발생하니 반드시 구체적인 origin을 명시)

만약 여러 개의 api에 같은 처리를 해주고 싶다면 전역으로 설정해준다.
(configCors 같은 폴더에 @CrossOrigin 함수 부분만 모듈화(?))

결론

현업에서는 CORS 상황이 되도록 발생하지 않도록 로직을 짠다고 한다.
그리고 발생한다 해도 보통 백엔드 쪽에서 해결하는 경우가 많다고 들었다. 프론트엔드에서는 SOP, CORS 개념과 해결방안 1번 정도만 기억해두면 좋을 것 같다.

profile
데이터 분석가 준비 중입니다 (티스토리에 기록: https://cherylog.tistory.com/)

0개의 댓글