CORS는 무엇일까?
Origin
--
- SOP (Same Origin Policy)
- CORS (Cross Origin Resource Sharing)
SOP와 CORS를 보면 둘 다 "Origin"이라는 단어가 포함되어 있다.
Origin이란 "출처"를 의미하고
URL에서 Protocol + Host + Port를 합친 주소를 Origin이라고 부른다.
즉, URL부분에서 출처를 판단할 때에는 Origin을 확인한다.
그래서 SOP와 CORS는 이 Origin(출처)와 관련 있는 용어라는 것을 알 수 있다.
--
SOP (Same Origin Policy, 동일 출처 정책)
--
SOP는
브라우저의 정책 중 하나로
웹 보안을 위해 클라이언트(브라우저)가
악의적인 사이트로부터 데이터를 탈취하거나 조작하는 것을 방지하기 위한 정책으로,
현재 클라이언트의 Origin과 요청 보내려는 백엔드 서버의 Origin이 동일해야만 요청이 가능하도록 한 정책이다.
즉, 클라이언트의 Origin이 요청 대상인 백엔드 서버의 Origin과 동일해야만 요청을 보낼 수 있으며
이를 통해 내가 모르고 악의적인 사이트에 접속하여
나의 브라우저에 저장된 쿠키나 다른 데이터들이 탈취 당하는 것을 방지할 수 있게 해 준다.
그래서 위 그림을 보면 두 Origin이 다르기 때문에 SOP로 인해 요청이 불가능하게 된다.
예시 상황
하나의 컴퓨터에서 react를 사용하여 프론트 서버를 실행하고, spring boot를 사용하여 백엔드 서버를 실행하면
기본적으로 각각 http://localhost.3000, http://localhost:8080의 Origin 주소를 가지게 된다.
두 Origin이 다르기 때문에 하나의 컴퓨터에서 실행했더라도 서로 통신이 불가능했던 것이다.
클라이언트의 Origin과 백엔드 서버의 Origin이 동일해지려면?
- 서로 다른 컴퓨터에서 실행되어야 한다.
- 같은 포트에서 실행되어야 한다.
- 동일한 도메인을 사용해야 한다.
하나의 컴퓨터에서 프론트 서버와 백엔드 서버를 실행하게 되면
서로 다른 포트를 사용해야 하므로 결코 동일한 Origin이 될 수 없다.
그래서 서로 다른 컴퓨터이면서 같은 포트로 실행해야 한다.
그리고 따로 도메인을 지정하지 않으면 기본으로 "localhost"라는 특별한 도메인으로 지정되는데
이것 또한 도메인이므로 서로 localhost라면 같은 도메인이라고 생각하면 된다.
만약 localhost 도메인마저 사용하지 않고 IP를 사용하게 된다면
서로 다른 공간의 컴퓨터에서 실행하므로 IP가 다르게 된다.
그래서 도메인이 아닌 IP를 사용하게 되면 두 서버의 Origin이 달라지는 것이므로 SOP에 걸리게 된다.
그래서 이러한 경우 도메인을 사용해야 한다.
하지만 보통 개발을 할 때면 하나의 컴퓨터에서 프론트 서버와 백엔드 서버를 동시에 실행하여
서로 잘 동작하는지 테스트를 하게 된다.
이러면 서로 Origin이 다르기 때문에 요청이 불가능하지만
이를 해결할 수 있는 방법들이 여러 가지 존재하지만 일반적으로 Proxy를 이용하는 방법을 사용한다.
Proxy 설정
프론트 서버에서 코드로 Proxy를 통해 백엔드 서버의 Origin을 정의하여,
클라이언트가 요청하게 되면 기존 클라이언트의 Origin을 Proxy에 정의한 백엔드 서버의 Origin으로 변환 후
백엔드 서버로 요청을 전송하기 때문에 서로 같은 Origin인 것처럼 SOP를 속여 회피할 수 있다.
SOP 검사 과정
- 프론트 서버가 백엔드 서버에 요청을 보냄
- 해당 요청을 바로 백엔드 서버에 전달하지 않고 브라우저가 해당 요청의 정보를 확인
- 요청 정보를 보고 출발지(프론트 서버)의 Origin과 목적지(백엔드 서버)의 Origin을 비교하여 SOP 검사 수행
- SOP 검사에 통과하면 그대로 해당 요청 백엔드 서버에 요청하여 정상 동작 수행
- SOP 검사에 실패하면 백엔드 서버에 요청을 전달하지 않고 오류 발생(CORS 에러 등)
출발지 Origin과 목적지 Origin을 비교할 때 String value(문자열 형태)로 비교하게 되어
만약 localhost 도메인의 IP가 127.0.0.1이라고 하고
출발지가 http://localhost:3000
목적지가 http://127.0.0.1:3000
이라고 해도 값이 아닌 문자열 형태로 비교하여 다르다고 판단한다.
요청 과정
기본 정보
- 프론트 서버의 Origin = http://localhost:3000
- 백엔드 서버의 Origin = http://localhost:8080
- /api/data API를 호출 요청하는 과정
[ Proxy를 사용하지 않은 경우 ]
- 프론트 서버 내에 목적지인 백엔드 서버를 명시해야 함 (목적지를 http://localhost:8080 으로 정의함)
만약 백엔드 서버를 명시하지 않으면?
기본적으로 자신의 주소(프론트 서버의 Origin)를 그대로 목적지 주소로 사용하기 때문에
자기 자신(프론트 서버)에게 요청하게 된다.
만약 자기 자신에게 요청을 보내게 된다면?
SOP 검사는 통과하겠지만
프론트 서버에는 해당 요청을 처리하는 로직이 없기 때문에
일반적으로 404 Not Found 응답을 반환하게 된다.
- 프론트 서버는 http://localhost:3000에서 http://localhost:8080/api/data로 요청 보냄
- 브라우저는 해당 요청을 보내기 전에 SOP 검사 수행
- 출발지 = http://localhost:3000
- 목적지 = http://localhost:8080
- 출발지와 목적지의 Origin이 다르다는 것을 확인하여 해당 요청을 차단 (해당 결과를 프론트 서버에 응답 X) - 브라우저는 SOP 검사 실패를 나타내는 "CORS 에러"와 같은 에러를 브라우저에 띄운다.
- 프론트 서버는 요청에 따른 응답을 아직 받지 않아 계속 응답을 기다리는 상황
(프론트 서버 내에서는 어느 기간동안 응답이 오지 않았을 경우에 대처하는 코드를 작성해야 좋다.)
[ Proxy를 사용하는 경우 ]
- Proxy를 사용하는 경우에는 프론트 서버 내에 목적지(백엔드 서버)를 명시하면 안 된다.
- Proxy에 목적지(백엔드 서버)를 명시한다.
만약 프론트 서버(출발지)의 Origin과 다른 목적지를 명시하면?
SOP 검사를 할 때 서로 다른 Origin을 검사하므로 SOP 검사 실패로 요청 차단이 이루어진다.
프록시 설정하는 방법?
react같은 경우 설정 파일인 package.json에서 "proxy": "http://localhost:8080"처럼
백엔드 서버의 Origin을 작성하여 프록시 주소를 정의한다.
- 프론트 서버는 http://localhost:3000에서 http://localhost:3000/api/data로 요청 보냄
- 브라우저는 해당 요청을 보내기 전에 SOP 검사 수행
- 출발지 = http://localhost:3000
- 목적지 = http://localhost:3000
- 출발지와 목적지의 Origin이 같다는 것을 확인하여 해당 요청을 전달 - http://localhost:3000/api/data 요청을 받은 프론트 서버는 프록시를 통해
http://localhsot:8080/api/data 로 변환하여 요청을 전달한다. - 요청을 받은 백엔드 서버는 해당 api를 수행하여 다시 프론트 서버로 응답
- 프론트 서버는 백엔드 서버로부터 받은 응답을 그대로 브라우저로 응답
- 브라우저는 받은 응답을 화면에 정상적으로 출력하여 모든 수행 끝
http://localhost:3000/api/data 요청은 왜 브라우저를 거치고
http://localhost:8080/api/data 요청은 왜 바로 백엔드 서버로 전달될까?
첫 요청은 사용자가 브라우저를 통해 API를 요청하는 것이라서 브라우저를 거쳐 SOP 검사를 하게 되는 것이고,
다음 요청은 프론트 서버가 API를 요청하는 것이라서 브라우저를 거치지 않고 바로 백엔드 서버로 요청한 것이다.
--
CORS (Cross Origin Resource Sharing, 교차 출처 리소스 공유)
--
CORS는
SOP의 제한을 완화하기 위해
특정 조건에서만 다른 출처의 요청을 허용하도록 하는 것이다.
만약 두 Origin이 달라 SOP 검사에 실패하더라고
CORS 설정으로 해당 Origin의 요청을 허용했다면 요청이 가능해지는 것이다.
SOP 검사 안에 단순히 출발지와 목적지의 Origin만 비교하는 검사만 있는 것이 아니라
추가로 CORS 규칙에 따라 요청 처리를 어떻게 할지 결정하는 단계도 포함되어 있다.
즉, 브라우저는 SOP 규칙을 적용할 때, CORS 규칙을 참고하여
만약 다른 Origin이라도 해당 요청을 허용할 수 있는지를 추가로 판단할 수 있다.
1. 출발지와 목적지의 Origin이 동일하면 바로 요청을 보냄 (SOP 검사 끝)
2. 출발지와 목적지의 Origin이 다르면 바로 SOP 검사 실패로 요청을 거부하는 것이 아니라
다음 단계인 CORS 규칙에 따라 어떻게 해당 요청을 처리할지 결정하게 된다.
CORS 설정은 "백엔드 서버"에서 설정하는 것으로
백엔드 서버에서 받을 요청에 대한 출발지를 명시하는 것이라고 생각하면 된다.
브라우저가 Cross-Origin 요청(CORS 요청)을 처리할 때 사용하는 3가지 유형
- Simple Request (단순 요청)
- Preflight Request (사전 요청)
- Credentialed Request (인증 정보 포함 요청)
위 3가지 유형들은
브라우저가 현재 요청의 정보를 보고
해당 요청에 알맞은 유형을 찾아 자동으로 해당 유형을 가지로 요청을 처리한다.
만약 현재 요청이 위 3가지 유형들 중 어느 유형에도 맞지 않으면,
브라우저는 해당 요청을 차단하고 백엔드로 전송하지 않는다.
그래서 브라우저는 SOP에 따라 해당 요청을 허용되지 않는다고 판단하여 "CORS 오류"를 발생시키고,
콘솔에 관련 에러 메시지를 출력하게 된다.
즉, SOP 검사의 모든 단계에 통과하지 못했으니 요청을 처리하지 않고 에러 발생
Simple Requets (단순 요청) 유형
현재 요청이 특정 조건을 충족하는 요청이라면
브라우저가 출발지와 목적지가 다른 경우에도 바로 백엔드 서버로 요청을 보낼 수 있다.
단순 요청 방법은
백엔드 서버에서 CORS 설정하는 것과 별개로 그냥 요청하는 방법이다. (CORS 설정과는 관계없음)
즉, 백엔드 서버에서 CORS 설정을 하든 말든 아무 상관없이 그냥 요청 처리를 해주는 방법이다.
단순 요청 조건
위 조건을 만족하는 요청이라면
브라우저는 바로 서버로 직접 요청을 보낼 수 있다.
- 프론트 서버는 http://localhost:3000에서 http://localhost:8080/api/data로 요청 보냄
- 브라우저는 해당 요청을 보내기 전에 SOP 검사 수행
- 출발지 = http://localhost:3000
- 목적지 = http://localhost:8080
- 출발지와 목적지의 Origin이 다르다는 것을 확인
- 다음 검사인 CORS 규칙을 참고하여 검사 진행
- 해당 요청을 보니 "Simple Request"유형의 조건에 만족한 것을 확인 - 브라우저는 바로 http://localhsot:8080/api/data로 요청을 전달한다.
사실 Simple Request 조건에 알맞은 요청을 찾기 어렵기 때문에
Simple Request 유형으로 요청을 처리하는 것을 보기 힘들다.
Preflight Request (사전 요청)
현재 요청이 "단순 요청"의 조건에 충족하지 않으면
그다음으로 조건을 확인하는 유형으로
브라우저가 보안 상의 이유로, 해당 요청을 바로 보내지 않고
먼저 백엔드 서버에게 해당 요청을 허용할 수 있는지 확인하기 위해
OPTIONS 메서드를 사용하여 "사전 요청"을 보내는 방법이다.
즉, 백엔드 서버에서 정의한 CORS 설정에서 해당 요청의 출발지가 명시되어 있는지(해당 요청을 받도록 설정했는지)
확인하기 위해 먼저 OPTIONS 메서드에
해당 요청에 담긴 Origin과 HTTP 메서드, 헤더 등을 담아
"사전 요청"용 OPTIONS 메서드를 구성하고 해당 메서드를 백엔드 서버에 요청 보낸다.
OPTIONS메서드를 요청받은 백엔드 서버는 해당 정보를 확인하고 자신의 CORS 설정에서 해당 Origin을 허용했는지 등
확인한 다음 해당 요청을 받을 수 있는지 없는지에 대한 응답을 브라우저에게 전달한다.
만약 요청 가능이라는 응답이 오면 기존 요청을 보내고
요청 불가능이라는 응답이 오면 그제야 SOP 검사 실패로 확정되어 요청 거부한다.
사전 요청 조건
그냥 "단순 요청"조건에 적합하지 않으면 무조건 "사전 요청"을 진행한다.
- 프론트 서버는 http://localhost:3000에서 http://localhost:8080/api/data로 요청 보냄
- 브라우저는 해당 요청을 보내기 전에 SOP 검사 수행
- 출발지 = http://localhost:3000
- 목적지 = http://localhost:8080
- 출발지와 목적지의 Origin이 다르다는 것을 확인
- 다음 검사인 CORS 규칙을 참고하여 검사 진행
- 해당 요청을 보니 "Simple Request"유형의 조건에 충족하지 않아 "PreFlight Request"를 수행
- 해당 요청에 담긴 기본적인 정보(Origin, HTTP메서드 등)를 가지고 OPTIONS()메서드 구성
- OPTIONS() 메서드를 서버로 요청 (사전 요청)
- 백엔드 서버는 사전 요청을 받고 CORS 설정을 확인하여 해당 요청을 받을 수 있는지 확인 후 응답 (가능 응답)
- 브라우저는 사전 요청에 대한 응답을 확인하여 기존 요청을 전달할 수 있는지 판단 (가능 응답을 확인) - 브라우저는 http://localhsot:8080/api/data로 요청을 정상 전달한다.
(만약 사전 요청에 대한 응답에 불가능이라는 응답이 오면 SOP 검사 실패로 "CORS 에러" 발생 후 끝)
Credentialed Request (인증정보 포함 요청)
해당 요청은 "Preflight Request"와 방법이 동일하다.
Credentialed Request 또한 백엔드 서버의 CORS 설정을 확인하기 위해 "사전 요청"을 보내야 한다.
둘의 차이는
요청에 Credential(쿠키, 인증 정보 등) 정보가 포함되어 있는지 없는지 차이일 뿐이다.
(없으면 "Preflight Request" 있으면 "Credentialed Request"라고 생각하면 된다.)
기본적으로 브라우저 요청에는 인증과 관련된 헤더를 담지 않는다. (보안적인 이유)
그래서 쿠키와 같은 인증 정보를 요청에 담아서 보내야 한다.
다만 요청에 Credential 정보가 포함되어 있으면 추가적인 처리가 필요하다.
- 백엔드 서버의 CORS 설정에서 요청 허가에 *(와일드카드) 불가
CORS 설정에서 허용할 Origin 설정에서 "모든 Origin 허용"을 의미하는 와일드카드(*)는 사용 불가능.
민감한 정보(쿠키, 인증 정보 등)를 포함하고 있는 요청이기 때문에
꼭 해당 요청의 Origin을 정확하게 CORS 설정에 명시해줘야 한다.
Access-Control-Allow-Origin에 와일드 카드 사용 불가 / 직접 Origin을 정확하게 명시
- 인증 관련한 정보를 요청하는 Origin에는 추가적인 설정이 필수 (해당 Origin으로 오는 요청에는 인증정보가 있다.)
CORS 설정에서 허용한 Origin에서
인증 관련한 정보를 함께 담아 요청하는 Origin에는
추가적으로 해당 Origin에서 오는 요청에서는 인증 관련한 정보를 함께 담아서 온다는 설정을 해서
응답할 때 해당 출처가 인증 관련한 정보를 갖는 출처임을 의미하는 헤더를 포함하도록 해준다.
Access-Control-Allow-Credentials 옵션의 값을 true라고 정의해야 한다.
--
'Terminology' 카테고리의 다른 글
STOMP (Simple/Streaming Text Oriented Messaging Protocol) (2) | 2024.12.01 |
---|---|
웹소켓 (WebSocket) (0) | 2024.11.25 |
[디자인 패턴] 팩토리 메서드 패턴 (Factory Method) (1) | 2024.11.01 |
[디자인 패턴] 싱글톤 패턴 (Singleton) (0) | 2024.10.30 |
디자인 패턴 (1) | 2024.10.28 |