CORS 개념 다시 잡기

Park sang woo·2024년 7월 26일
0

CS스터디

목록 보기
9/25
post-thumbnail

📘 CORS (Cross-origin-resource-sharing)

CORS가 발생하는 이유는 웹 브라우저는 HTTP 요청에 대해서 어떤 요청을 하느냐에 따라 각기 다른 특징을 가지고 있기 때
문에 발생합니다.


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

1️⃣ <img>, <video>, script, link 태그 등

  • <link> 태그의 href에서 다른 사이트의 .css 리소스에 접근하는 것이 가능.
  • <img> 태그의 src 에서 다른 사이트의 .png, .jpg 등의 리소스에 접근하는 것이 가능.
  • <script> 태그의 src에서 다른 사이트의 .js 리소스에 접근하는 것이 가능

2️⃣ XMLHttpRequest, Fetch API 스크립트
기본적으로 Same-Origin 정책을 따름.

  • 다른 도메인의 소스에 대해 자바스크립트 ajax 요청 API 호출 시
  • 웹 폰트 CSS 파일 내 @font-face에서 다른 도메인의 폰트 사용 시

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

  • 이유 : 브라우저는 기본적으로 하나의 서버 연결만 허용되도록 설정되어 있기 때문.


이미지를 가져올 때 <img> 태그의 src 속성으로 가져오는 방식과 JS에서 ajax요청으로 가져오는 방식의 코드입니다.

<body>
    <img src="https://third-party-test.glitch.me/check.svg" alt="이미지">

    <script>
        fetch('https://third-party-test.glitch.me/check.svg')
            .then(response => response.blob())
            .then(imgBlob => {
                const imageObjectURL = URL.createObjectURL(imgBlob);
                const img = document.createElement('img');
                img.src = imageObjectURL; 
                document.body.append(img);
            })
    </script>
</body>

이러면 이미지가 한 번만 출력이 됩니다.


CORS 에러 메세지를 보면
Access to fetch at ‘https://myhompage.com’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
이러한 에러가 발생합니다.

해석해보면 'https://myhomepage.com'에서 'https://localhost:3000' 출처로 가져올 수 있는 액세스가 CORS 정책에 의해 차단되었습니다. 요청된 리소스에 'Access-Control-Allow-Origin' 헤더가 없습니다. 불투명한 응답이 필요에 적합한 경우, 요청 모드를 'no-cors'로 설정하여 CORS가 비활성화된 리소스를 가져오십시오. 라는 의미입니다.






📘 Same Origin / Cross Origin 정책 복습.

🎧 출처 (Origin)

우리가 어떤 사이트를 접속할 때 인터넷 주소창에 우리는 URL이라는 문자열을 통해 접근하는데 이 URL은 하나의 문자열이 아닌 구성 요소가 존재합니다.

Protocal(Schema) : http, https
Host : 사이트 도메인
Port : 포트 번호

사이트 내부 경로, 요청의 key와 value값, 해시 태그

Origin은 딱 3개 Protocol + Host + Port입니다.



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

동일한 출처에서만 리소스를 공유할 수 있다는 의미입니다.
동일 출처 서버에 있는 리소스는 자유롭게 가져올 수 있지만, 다른 출처 서버에 있는 이미지나 영상은 상호작용이 불가능하다는 의미입니다.


동일 출처 정책이 필요한 이유
출처가 다른 두 애플리케이션이 소통할 수 있는 환경은 위험합니다. 제약이 없다면, 해커가 CSRF, XSS 등의 방법을 이용해서 우리가 만든 애플리케이션에서 해커가 심어놓은 코드가 실행하여 개인 정보를 가로챌 수 있습니다.

그림을 통해 보면

  • 사용자가 악성 사이트에 접속.
  • 해커가 몰래 심어놓은 악의적인 JS가 실행되어, 사용자가 모르는 사이에 포털 사이트로 요청을 보냄.
  • 포털 사이트에서 해당 브라우저의 쿠키를 이용하여 로그인 하거나 등 상호작용에 따른 개인 정보를 응답 값을 받은 뒤, 사이트에서 해커 서버로 재차 보낸다.
  • 이외에도 사용자가 접속 중인 내부망의 IP, Port를 가져오거나, 해커가 사용자 브라우저를 프록시처럼 악용 가능.

이렇게 SOP로 다른 출처의 스크립트가 실행되지 않도록 브라우저에서 사전에 방지하는 것입니다.



🎧 같은 출처와 다른 출처 구분 기준

Origin이 동일한지로만 판단을 합니다.

이러한 출처 비교와 차단은 브라우저가 합니다. (서버라고 오해 X)

  • 서버에 요청을 했는데 무언가 에러가 발생하면 서버가 문제라고 생각을 해버리기 때문.
  • 응답 데이터는 멀쩡하지만 브라우저에서 받을 수 없도록 차단한 것.

클라이언트 단 코드에서 API요청을 하는게 아니라, 서버 단 코드에서 다른 출처의 서버로 API 요청을 하면 CORS에러를 겪지 않게 된다. 이를 이용한 프록시 서버라는 것이 있다.



🎧 CORS (교차 출처 리소스 공유)

다른 출처의 리소스 공유에 대한 허용/비허용 정책입니다.
개발하다보면 기능상 어쩔 수 없이 다른 출처 간의 상호작용을 해야 하는 케이스가 존재하고 실무적으로 다른 회사의 서버 API를 이용해야 하는 상황이 존재합니다.

SOP 정책을 위반해도 CORS 정책을 따르면 다른 출처의 리소스라도 허용을 하도록 해준다는 것입니다.



🎧 브라우저에서 CORS 동작

  1. 클라이언트에서 HTTP 요청의 헤더에 Origin을 담아 전달합니다.
  • 웹은 HTTP 프로토콜을 이용해서 서버에 요청을 보내는데 이때 브라우저는 요청 헤더에 Origin이라는 필드에 출처를 함께 담아서 보냅니다.
  1. 서버는 응답헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달합니다.
  • 서버가 이 요청에 대한 응답을 할 때 응답 헤더에 Access-Control-Allow-Origin이라는 필드를 추가하고 값으로 이 리소스를 접근하는 것이 허용된 출처 URL을 내려보냅니다.
  1. 클라이언트에서 Origin과 서버가 보내준 Access-Control-Allow-Origin을 비교합니다.
  • 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교한 후 차단할지 말지를 결정합니다.
  • 유효하지 않다면 그 응답을 사용하지 않고 버립니다. (CORS 에러)
  • 둘다 출처가 같기 때문에 유효하므로 다른 출처의 리소스를 문제없이 가져올 수 있게 됩니다.


🎧 CORS 해결책은 서버의 허용이 필요

이게 결국 해결책입니다
서버에서 Access-Control-Allow-Origin 헤더에 허용할 출처를 기재해서 클라이언트에 응답하면 됩니다.






📘 CORS 작동 3가지 시나리오

🎧 예비 요청 (Proflight Request)

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

  • 안전한 요청인지 미리 확인.
  • 이 예비 요청의 HTTP 메소드를 GET이나 POST가 아닌 OPTIONS라는 요청이 사용된다는 것이 특징입니다.


1️⃣ JS로 API 요청을 보낼 때 JS의 fetch() 메서드를 통해 리소스를 받아옵니다.

2️⃣ 브라우저는 서버로 OPTIONS 메서드로 예비 요청을 먼저 보낸 후 Origin 헤더에 자신의 출처를 넣습니다.
Access-Control-Request-Method/Headers 에 실제 요청에 사용할 메서드와 헤더들을 설정합니다.

3️⃣ 이후 서버는 이 예비 요청에 대한 응답으로 어떤 것을 허용하고 어떤 것을 금지하고 있는지에 대한 헤더 정보를 담아서 브라우저로 보내줍니다.

4️⃣ 브라우저는 보낸 요청과서버가 응답해준 정책을 비교해서 해당 요청이 안전한지 확인하고 서버가 본 요청에 대한 응답을 하면 최종적으로 이 응답 데이터를 JS로 넘겨줍니다.


JS코드로 API 요청을 보내면, 크롬 개발자 도구에서 클라이언트와 서버가 본 요청(xhr)을 보내기 전에 예비 요청 통신을 하고 있는 것을 볼 수 있습니다.

Origin과 Access-Control-Aloow-Orign 출처를 비교!!!
이 요청이 다르게 되면 CORS 정책을 위반했다고 에러가 발생하는 것입니다.



⭕ 예비 요청의 문제점과 캐싱

예비 요청으로 OPTIONS 메서드로 예비 요청을 보내 보안을 강화하는 목적은 좋지만 결국 실제 요청에 걸리는 시간이 늘어나게 되어 애플리케이션 성능에 영향을 미치는 단점이 존재합니다.

  • API 호출 수가 많아지면 수만큼 더 증가.

따라서 브라우저 캐시를 이용해서 Access-Control-Max-Age 헤더에 캐시될 시간을 명시해주면, 이 PreFlight 요청을 캐싱시켜 최적화가 가능합니다.

  • 브라우저는 예비 요청을 보낼 때마다, 먼저 PreFlight 캐시를 확인하여 해당 요청에 대한 응답이 있는지 확인.
  • Max-Age 응답 헤더를 받으면 그 기간 동안 브라우저 캐시에 결과를 저장



🎧 단순 요청(Simple Request)

예비 요청을 생략하고 바로 서버에 직행으로 본 요청을 보낸 후, 서버가 이에 대한 응답 헤더에 Access-Control-Allow-Origin 헤더를 보내주면 브라우저가 CORS 정책 위반 여부를 검사합니다.

  • 요청의 메서드는 GET, HEAD, POST 중 하나.
  • AcceptAccept-LanguageContent-LanguageContent-TypeDPRDownlinkSave-DataViewport-WidthWidth 헤더일 경우 에만 적용.
  • Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain중 하나여야한다. 아닐 경우 예비 요청으로 동작.

대부분 HTTP API 요청은 text/xml 이나 application/json 으로 통신하기 때문에 Content-Type이 위반된다.
따라서 그냥 예비 요청을 이루어진다라고 이해.



🎧 인증된 요청

클라이언트에서 서버에게 자격 인증 정보(Credential)를 실어 요청할 때 사용되는 요청입니다.

  • 자격 인증 정보 -> 세션 ID가 저장되어 있는 쿠키 혹은 Authorization 헤더에 설정하는 토큰 값.

요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 credentials 옵션.


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

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



클라이언트에서 인증 정보 보내기
fetch 메서드

fetch("https://example.com:1234/users/login", {
	method: "POST",
	credentials: "include", // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
    body: JSON.stringify({
        userId: 1,
    }),
})

axios 라이브러리

axios.post('https://example.com:1234/users/login', { 
    profile: { username: username, password: password } 
}, { 
	withCredentials: true // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
})

jQuery 라이브러리

$.ajax({
	url: "https://example.com:1234/users/login",
	type: "POST",
	contentType: "application/json; charset=utf-8",
	dataType: "json",		
	xhrFields: { 
    	withCredentials: true // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
    },
	success: function (retval, textStatus) {
		console.log( JSON.stringify(retval));
	}
});

서버에서 인증된 요청에 대한 헤더 설정하기
서버도 인증된 요청에 대해 일반적인 CORS 요청과는 다르게 대응해야 합니다.

  • 응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정해야 한다.
  • 응답 헤더의 Access-Control-Allow-Origin 의 값에 와일드카드 문자("*")는 사용할 수 없다.
  • 응답 헤더의 Access-Control-Allow-Methods 의 값에 와일드카드 문자("*")는 사용할 수 없다.
  • 응답 헤더의 Access-Control-Allow-Headers 의 값에 와일드카드 문자("*")는 사용할 수 없다.

즉 분명한 Origin으로 설정해야 합니다.

이러한 에러들을 발견했다면 반드시 위 사항들을 확인해보도록!!!



🎧 CORS 3가지 시나리오 코드 작동 (빠르게 이해 가능)

🖇️ https://chuckchoiboi.github.io/cors-tutorial/


🎧 CORS 해결 방법 정리

🖇️ CORS 해결 방법 정리






Reference

🖇️ CORS 정리와 해결법

profile
일상의 인연에 감사하라. 기적은 의외로 가까운 곳에 있을지도 모른다.

0개의 댓글