슈슉 슈슉. 슉. 슉.C..C.CORS 놈아

EntryDSM·2022년 6월 8일
107

안녕하세요, 대덕소프트웨어마이스터고등학교의 입학전형 시스템의 어드민 서버를 개발한 3학년 이승윤입니다!
꾸준히 팀 내에서 스스로의 책임을 되물으며 능동적인 팔로워가 되기 위해 노력하고 있습니다.
글 내용 중 오류가 있는 경우 여기로 연락해주시면 재빨리 답변드리겠습니다.


또 CORS라고? 🤯

웹 프로젝트를 진행하면 브라우저에서 CORS 오류를 종종 볼 수 있습니다.

대체 CORS가 뭐길래 우리를 이렇게 힘들게 하는 걸까요?

CORS 개념을 이해하기 전, SOP에 대해 먼저 살펴보겠습니다.

SOP 넌 누구니? 🤨

SOP는 Same Origin Policy의 줄임말입니다. 즉, ‘같은 출처 정책’을 말합니다.

같은 출처에서만 자원들을 공유할 수 있도록 하는 자바스크립트 엔진 표준 스펙의 정책입니다.

자바스크립트 엔진이란, 쉽게 말해 자바스크립트 코드를 실행하는 프로그램으로서 주로 웹 브라우저를 위해 사용되는 것입니다.

자바스크립트 엔진 표준 스펙의 정책이기 때문에 태그 안에 둘러싸인 것 외의 다른 정보는 SOP의 관여 없이 GET 메소드를 사용하여 불러올 수 있습니다.

같은 출처에서만 자원을 공유해야 한다니 요래 저래 불편하네요. 하지만 누구든지 내 정보를 가지고 갈 수 있다면 어떨까요?

계정의 비밀번호를 가져가는 스크립트(XSS, Cross-Site Scripting)를 만들지도 모릅니다.

이와 같은 상황을 방지하고 보안을 지키기 위해 만들어진 정책이 SOP입니다.

같은 출처? 기준이 뭐지 🤔


위 사진과 같이 URL에는 Protocol, Host, Port, Path 등으로 이루어져 있습니다.

같은 출처인지 비교하기 위해서는 두 URL의 Protocol(Scheme), Host(Domain), Port 이 세 가지 요소가 모두 동일해야 합니다.

URL의 포트 번호는 생략되는 경우가 많습니다. HTTP는 80, HTTPS는 443으로 기본 포트가 정해져 있기 때문입니다.

예를 들어, 로컬에서 Spring boot와 React를 실행시키면 따로 설정하지 않는 이상 Spring boot는 http://localhost:8080으로 기본 실행되고, React는 http://localhost:3000으로 실행됩니다.

만약 로컬의 React에서 로컬의 Spring boot 서버에 접근하면 어떨까요?

두 URL의 Protocol(http://)과 Host(localhost)는 같지만 Port가 달라서 CORS를 따로 설정해주지 않으면 오류가 발생할 것입니다.

그럼 CORS는 뭐야? 🤓

SOP 정책에 몇 가지 예외 상황을 두어 다른 Origin끼리도 자원을 주고받을 수 있도록 하는 정책이 CORS(Cross-Origin Resource Sharing)입니다.

이전에는 동일한 도메인에서만 자료를 받아왔지만, 클라이언트에서 도메인이 다른 서버를 사용하는 일이 많아지면서 CORS가 등장했습니다.

서버 측에서 CORS를 설정하면, 허용된 Origin(출처), 허용된 Method(메소드)로는 접근이 가능해집니다.

CORS 동작원리 🤖

대표적인 CORS 동작으로는 Simple Request, Preflight가 있습니다.

클라이언트에서 따로 설정해주지 않아도 W3C 사양에 따라 브라우저는 지원되는 메소드가 서버에서 유효한지 확인하기 위해 실제 요청을 보내기 전에 OPTIONS 요청을 수행합니다 (https://medium.com/@f2004392/cors-preflight-request-options-9d05b9248e5a).

Simple Request

먼저, 단순 요청으로 보낼 때는 다음 세 가지 조건이 모두 충족해야 합니다.

  • GETHEADPOST 메서드를 사용해야 합니다.
  • User agent에서 자동으로 설정한 헤더 외에 수동으로 설정할 수 있는 헤더는 Fetch에 정의되어 있는 헤더 뿐입니다.
  • 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용됩니다.

Simple Request의 시나리오를 따르기 위해서는 사용자 인증을 위한 Authorization도 사용할 수 없으며, 대부분의 HTTP 요청에서 사용하는 Content-Typeapplication/json도 설정할 수 없습니다.

Preflight

Simple Request의 까다로운 조건을 다 맞추는 상황을 만들기는 쉽지 않습니다. 그래서 우리는 preflight을 보내는 방식을 주로 사용하게 됩니다.

여기서 preflight란, 사전 요청 정도로 생각하시면 됩니다.

  1. 자바스크립트의 Fetch API를 사용하여 브라우저에게 리소스를 받아오라고 명령을 보내면, 브라우저는 실제 요청을 보내기 전 OPTIONS라는 HTTP 메소드를 사용하여 사전 요청을 보냅니다.

    OPTIONS는 목표 리소스와의 통신 옵션을 설명하기 위해 사용되는 메소드입니다.

  2. 서버는 이에 대한 응답으로 어떤 메소드, 출처 등을 허용하는지에 대한 정보를 헤더에 담아 반환합니다.
  3. 그럼 브라우저는 이때 받은 응답과 보낼 요청을 비교한 후 요청을 보내도 안전하다고 생각하면 실제 요청을 보내고 서버에게 받은 응답 데이터를 자바스크립트로 전달해줍니다.

Credentialed Request 🔐

사전 요청의 응답으로 사용할 때, 실제 요청에서 자격증명을 이용할 수 있는지에 대해서 알려줍니다.

서버에서 쿠키를 따로 사용하는 것이 아니라면, credentials를 사용하지 않아도 요청/응답이 가능합니다.

같은 출처에서 HTTP 통신을 하는 경우 별도의 설정 없이 쿠키가 요청 헤더에 들어갑니다. 하지만 CORS는 기본적으로 보안상의 이유로 쿠키를 요청으로 보낼 수 없도록 막고 있습니다. 따로 쿠키에 인증과 관련된 정보를 담을 수 있게 해주는 옵션인 credentials을 설정해주어야 정상적인 응답을 받을 수 있습니다.

credentials의 옵션은 3가지가 있습니다.

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

Preflight Request의 응답에서 Access-Control-Allow-Credentials: false이거나 생략한 경우, 실제 요청을 서버에 보낼 때에는 credentials 정보를 함께 보내지 않습니다.

Simple GET 요청의 경우 사전 요청 과정이 없기 때문에 credentials 정보와 함께 CORS 요청을 합니다. 단, 서버에서 서버에서 Access-Control-Allow-Credentials: false이거나 생략한 경우, 브라우저는 받은 응답을 자바스크립트 코드에 전달하지 않습니다.

Access-Contol-Allow-Credentials: true로 설정했다면 동일 출처 여부와 상관없이 모든 요청에 인증 정보가 포함되도록 설정된 것이기 때문에 Access-Control-Allow-Origin: *는 불가능합니다.

추가 정보 ➕

  • CORS는 클라이언트 측 웹 애플리케이션의 보안정책이기 때문에 앱과 통신하는 경우, Postman 등을 사용하여 서버끼리 통신하는 경우에는 적용되지 않습니다.
  • 기본적으로는 SOP만 적용되므로 몇 가지 예외 상황을 두기 위해서는 서버 측에서 CORS 설정을 해주어야 합니다.

CORS 이슈 해결방법 🦸

CORS의 개념만 설명하면 아쉬우니 CORS를 해결하는 다양한 방법 중에서 Entry DSM의 기술 스택인 Spring boot를 기준으로 해결 방법 두 가지 경우를 말씀드리겠습니다.

Controller마다 @CrossOrigin 붙이기

@CrossOrigin(origins = "http://localhost:3000") //'origins ='는 생략 가능
@RestController
public class TestController {

    @GetMapping
    public void test() {}

}

@CrossOrigin은 Spring에서 CORS를 적용할 수 있게 만든 어노테이션입니다. origins 속성으로 허용 출처를 허용하면 다른 Origin이더라도 요청을 주고 받을 수 있게 됩니다.

위의 코드처럼 클래스 단에서 어노테이션을 추가할 수도 있고, 각 메소드마다 설정해줄 수도 있습니다. 하지만, 컨트롤러가 많을 수록, 기능이 많을 수록 설정해야 하는 어노테이션이 많아진다는 단점이 있습니다.

WebConfig에서 전역으로 설정하기

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedMethods("GET", "POST", "DELETE", "PATCH", "PUT")
                .allowedOrigins("http://localhost:3000", "http://localhost:3001",
						"http://localhost:3002", "https://apply.entrydsm.hs.kr", "https://admin.entrydsm.hs.kr");
    }

}

현재 Entry DSM에서 사용하는 방법입니다. 특정 도메인으로 오는 요청에 대해 전역으로 CORS를 적용하고 싶은 경우 사용합니다. WebMvcConfigurer의 구현체를 빈으로 등록한 후 addCorsMappings 메소드를 오버라이딩하면 간편하게 설정할 수 있습니다.

  • .addMapping() : CORS를 적용할 URL 패턴을 정의합니다. Entry DSM에서는 모든 요청의 CORS를 전역으로 설정하기 위해 “/**”로 설정하였습니다.
  • allowedMethods() : 허용할 Http Method를 지정합니다. 모든 메소드를 허용하기 위해서는 ”*만 적어주면 됩니다.
  • allowedOrigins() : 자원 공유를 허락할 출처를 지정합니다. 모든 출처를 허용하려면 ”*만 적어주면 됩니다. Entry DSM에서는 React의 로컬 주소와 배포 주소만 허락하였습니다.

추가로 로컬 환경에서 API를 사용 시,

Allow CORS: Access-Control-Allow-Origin

위의 프로그램을 활성화시키게 되면, CORS 문제를 해결할 수 있다고 합니다.

모든 메소드를 허용하고, 모든 출처를 허용하는 것은 보안적으로 절대 좋은 방법이 아닙니다. 각자 사용하는 언어/프레임워크에서 CORS를 해결하는 방법을 찾아보고 최소한으로 허용하는 것을 추천드리며 이 글을 마치도록 하겠습니다.



참고

profile
대덕소프트웨어마이스터고등학교의 입학전형시스템을 개발하고 있는 팀 EntryDSM입니다. https://velog.io/@entrydsm 에 이어 전공 지식을 공유하기 위해 블로그를 개설하게 되었습니다.

5개의 댓글

comment-user-thumbnail
2022년 6월 8일

그동안 골치 아팠던 CORS 문제에 대해 쉽고 정확하게 알려주셔서 감사합니다!!
덕분에 많은 도움이 되었어요 ~

답글 달기
comment-user-thumbnail
2022년 6월 9일

너무나 강력한 타이틀과 썸네일에,,,, 지나가다 웃으며 들어왔는데,,, ㄴ0ㄱ! 코린이는 눈물 닦으며 돌아갑니다,,,

답글 달기
comment-user-thumbnail
2022년 6월 9일

👍👍👍

답글 달기
comment-user-thumbnail
2022년 6월 15일

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

답글 달기
comment-user-thumbnail
2022년 6월 16일

썸네일, 제목에 웃고 내용에 감탄했습니다 : )

답글 달기