주제 선정 이유
처음 서버와 연동하는 웹 프로젝트를 했을 때, 개발자 도구 콘솔 창에 다음과 같은 에러가 자주 발생했었다. 그때 클라이언트의 문제인지, 서버의 문제인지 잘 몰라서, 프론트를 맡은 내가 해결할 수 있는 방법으로 어찌저찌 해치웠던 경험이 있었다.
그 이후로도 제대로는 이해하지 못하고 넘어간 문제였는데, 브라우저 동작 방식에 대해 조금 더 이해하게 된 지금은 확실하게 정리해볼 수 있을 것 같아 위와 같은 주제를 선정했다.
CORS가 뭐야?
에러를 파악하기 위해서는 우선 CORS가 무엇인지부터 이해해야 한다. MDN에서는 CORS를 다음과 같이 소개하고 있다.
Cross-Origin Resource Sharing(CORS, 교차 출처 리소스 공유)
|
출처란?
출처(origin)는 도메인 + 프로토콜 + 포트를 합친 것이다. ex) http://example.com:80
위 3개만 동일하면 같은 출처이므로, 다음 둘의 url이 같은 출처임을 유의하자.
ex) http://example.com/app1/index.html 과 http://example.com/app2/app.html 은 같은 출처!
출처 정책에는 cross-orign 정책과 same-origin 정책이 있다.
- cross-orgin 정책
- 다른 도메인의 리소스에 접근할 수 있음.
- <img>, <video>, <script>, <link> 태그 등이 해당.
- same-orgin 정책 (SOP 정책)
- 다른 도메인의 리소스에 접근 불가.
- XMLHttpRequest, Fetch API 스크립트 등이 해당.
보안의 이유로 브라우저는 스크립트에서 시작한 cross-orgin HTTP 요청을 제한하며, 기본적으로 하나의 서버 연결만 허용한다. 동일 출처 정책을 따르는 웹 애플리케이션에서 다른 출처의 리소스를 불러오려면, 그 출처(다른 출처)에서 올바른 CORS 헤더를 포함한 응답을 반환해야 한다.
Why SOP?
그럼 동일 출처 정책을 굳이 사용하는 이유는 뭘까? 바로 보안 취약점을 막기 위해서라고 한다. 이러한 제약이 없다면 CSRF나 XSS와 같은 해킹 방법을 이용해서 코드를 심고, 그 코드가 실행되어 개인 정보를 가로챌 수 있다.
CSRF(Cross-Site Request Forgery)
- 사용자가 자신의 의지와는 무관하게 자신의 권한으로 공격자가 의도한 행위를 특정 웹사이트에 요청하게 하는 공격.
- 공격자는 사용자가 이미 인증한 세션을 악용하여 사용자가 원하지 않는 요청을 수행하도록 함.
- ex) 사용자가 악성 스크립트 페이지를 실행하게 하여 session ID 등의 정보를 갈취한 후 서버에 요청하여 사용자의 데이터를 수정 및 삭제함.
XSS(Cross-Site Scriping)
- 웹 애플리케이션에 악성 스크립트를 삽입하여 사용자의 브라우저에서 실행되도록 함
위 이미지처럼 사용자가 특정 script가 심어진 evil.com 페이지에 접속했다면, 접속과 동시에 script가 실행되어 AJAX 호출로 은행 API를 호출할 수 있다. 그러면 공격자가 나의 은행 계좌를 삭제해버릴 수도 있는 것이다.
이러한 보안상의 약점에도 불구하고, 클라이언트에서 출처가 다른 서버에서 제공하는 API를 사용할 일은 자주 존재한다. 따라서 다른 도메인으로도 요청하기 위해 CORS라는 해결법이 존재한다.
CORS의 동작 방식
CORS 요청에는 Simple Request와 Preflighted Request 의 두 방식이 존재한다.
Simple Request
simple request 방식을 사용하기 위해서는 다음과 같은 조건을 만족해야 한다.
- GET, HEAD, POST 중 하나의 메서드를 사용한다.
- 브라우저가 자동으로 만드는 Request Header 이외에는 다음 헤더만 추가할 수 있다.
- Accept
- Accept-Language
- Content-Language
- Content-Type
- 헤더가 Content-Type 일 경우 헤더를 다음의 값들로만 작성해야 한다는 추가조건이 있다. 다음의 3가지 타입은 브라우저에서 기본적으로 안전하다고 간주되어 바로 요청이 가능하다.
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
요청은 다음과 같이 이루어진다.
1. 클라이언트에서 다른 출처의 서버에 보낼 요청을 준비한다. 요청은 XMLHttpRequest나 fetch API를 사용하여 만들 수 있다.
function simpleRequest(){
const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/public-data/';
xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();
}
2. 브라우저에서 클라이언트의 js 코드를 실행하여 실제 HTTP 요청을 생성한다. 이때 해당 요청에 host와 orgin request header 등을 추가하여 서버로 보낸다.
## browser request
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
여기서 Host 헤더는 요청이 전송될 서버의 출처를, Origin 헤더는 요청이 시작된 서버의 출처(클라이언트 서버의 출처)를 말한다.
3. 서버에서 origin 요청 헤더를 확인한다. orgin 요청 값이 허용되면, Access-Control-Allow-Origin 헤더를 추가하여 응답한다. 해당 해더 값으로는 모든 도메인에서 접근할 수 있는 *로 응답하거나, orgin 헤더로 응답하는 방법이 있다.
HTTP/1.1 200 OK #요청 처리함
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: * #이때 https://foo.example로 설정할 수도 있다!
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
4. 브라우저는 서버의 응답을 받고, 클라이언트 코드에 응답을 전달한다.
Preflighted Request
위의 요청은 원하는 출처의 서버에 바로 요청을 보내는 것이지만, 이 요청은 그렇지 않다. 우선 OPTIONS 메서드를 통해 다른 도메인의 리소스로 HTTP 요청을 보내서 실제 요청이 전송하기에 안전한지 확인한다.
preflighted request는 cross origin 요청이 서버의 유저 데이터에 영향을 줄 수 있을 경우에 사용한다. 즉 simple request의 조건을 충족하지 못하는 경우에는 모두 preflight 처리를 해야한다.
1. 클라이언트 서버에서는 다음과 같이 요청을 보낸다.
const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/post-here/';
xhr.open('POST', url);
xhr.setRequestHeader('X-PINGOTHER', 'pingpong'); // custom header
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
위 코드는 POST 요청이고, 요청과 함께 보낼 XML body를 만들고 있다. 또한 Ping-Other이라는 비표준 HTTP 요청 헤더를 설정했다. content-type이 application/xml 이고, 사용자 정의 헤더가 있으므로 preflighted 처리된다.
2. 브라우저는 클라이언트 코드가 preflight 요청을 보내야 하는지 확인하고, 실제 요청 전에 OPTIONS 요청으로 preflight 요청을 서버로 보낸다. 서버가 해당 요청을 허용하는지 확인하기 위함이다.
OPTIONS /resources/post-here/ HTTP/1.1 #OPTIONS 메서드를 사용한 preflight request임을 나타냄
Host: bar.other #실제 요청을 전송할 서버의 출처
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST #실제 요청을 전송할 때 사용할 메소드
Access-Control-Request-Headers: X-PINGOTHER, Content-Type #실제 요청을 전송할 때 사용할 헤더
preflight 요청은 밑의 두 줄이 핵심이다. 실제 요청을 어떤 식으로 전달할지 미리 알려주는 부분인데, Access-Control-Request-Method 헤더는 POST 메서드로 전송된다는 것을, Access-Control-Request-Headers는 사용자 정의 헤더와 같이 전송된다는 것을 알려준다.
request header의 X-pingother는 클라이언트 코드에서 정의한 사용자 정의 헤더 이름이다. 이름은 해당 js 코드에서 아무거나 설정하면 된다!
3. 서버는 preflight 요청을 받고 실제 요청을 허용할지의 여부를 알려준다.
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example #실제 요청을 전송할 출처
Access-Control-Allow-Methods: POST, GET, OPTIONS #실제 요청을 전송할 때 사용가능한 메서드
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type #사용자 지정 헤더
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Access로 시작하는 응답 코드를 살펴보면, simple 요청과 비교하여 origin 코드 이외에 3개의 코드이 추가된 것을 볼 수 있다.
- Access-Control-Allow-Methods : 클라이언트가 요청한 리소스에 대해 POST, GET, OPTIONS 메서드를 사용할 수 있다고 알려준다.
- Access-Control-Allow-Headers : 실제 요청에 사용자 정의 헤더를 사용할 수 있음을 알려준다.
- Access-Control-Max-Age : preflight 요청에 대한 응답을 캐시할 수 있는 시간(요청을 다시 보내지 않아도 되는 시간)을 알려준다. (위 시간은 24시간!)
4. 브라우저는 preflight 응답을 확인하고 실제 요청을 보낸다.
POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache
<person><name>Arun</name></person>
5. 서버는 실제 요청을 처리한 후 브라우저로 응답을 보내고, 브라우저는 이를 클라이언트로 보낸다.
## server real response
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
그럼 CORS 에러는?
이제 우리는 CORS가 무엇인지, 왜 사용하는지, 동작방식이 무엇인지 이해했다. 그럼 CORS 에러가 무엇인지는 쉽게 알 수 있다. CORS 정책을 위반하면 발생하는 에러인 것이다.
에러, 어떻게 해결할까?
우선 내가 했던 방법에 대해 소개하고자 한다. 내가 마주친 CORS 에러는 개발 환경에서 이미 배포한 서버의 Access-Control-Allow-Origin에 내 로컬 서버를 추가해주지 않아서 발생한 문제였다. 하지만 실제 배포한 웹에서는 하나의 api 주소만 사용할 것이므로, proxy를 이용하여 로컬 서버를 요청할 서버 주소로 변경했다.
//package.json
{ #요청할 서버 주소
"proxy": "http://apiserver.com:5000",
}
//요청 메소드
export const getAPI = async () => {
// const response = await fetch("http://apiserver.com:5000/api");
const response = await fetch("/api");
return await response.json();
};
위와 같은 내용을 React 프로젝트의 package.json 항목에 추가해주면 된다. 그럼 다음과 같은 방식으로 요청이 처리된다.
1. 브라우저는 클라이언트 코드를 읽고 요청을 React 개발 서버(요청과 동일한 서버 주소)로 전송한다.
2. React 개발 서버는 package.json의 프록시 설정을 확인하고, 해당 프록시 주소로 자신이 받은 요청을 보낸다.
3. API 서버는 요청을 처리한 후 응답을 생성하여 React 개발 서버로 전송한다.
4. React 개발 서버는 받은 응답을 브라우저로 전달한다.
5. 브라우저는 React 개발 서버로 받은 응답을 클라이언트 코드로 전달한다.
위의 단순한 코드 한 줄로 나의 문제를 해결할 수 있었다. 위의 방법은 하나의 api 주소만 사용한 것인데, 이외에도 webpack dev server의 proxy 기능을 사용하거나, http-proxy-middleware를 사용하는 등의 다양한 방법이 존재한다.
하지만 이러한 프록시 설정은 개발 환경에서만 가능하다. 실제 배포 시 프록시 설정을 클라이언트에 두는 것은 보안 문제가 있고, 정적으로 정의되어 있기 때문에 런타임에 동적으로 변경할 수 없어 유연하지 못하다. 실제 배포를 위해서는 어떻게 문제를 해결하는지 알아보자.
서버에서 Access-Control-Allow-Origin 헤더 세팅하기
가장 정석적인 해결 방법이다. 아주 다양한 세팅 방식이 있었고, 서버마다 방식이 조금씩 다르다. 내가 서버에 대한 지식이 많지 않으므로, Spring에서 어떻게 설정하는지만 알아보았다.
@CrossOrigin(origin="*", allowedHeaders = "*")
@Controller
public class MainController {
@GetMapping(path = "/")
public String main(Model model) {
return "main";
}
}
@CrossOrigin annotation에 origins, allowHeaders 등의 속성 값을 넣어 CORS 에러를 해결할 수 있다.
- origins : access-control-allow-origin에 배치될 값
- allowedHeaders : access-control-allow-header에 배치될 값
프록시 서버 이용(구축)하기
서버를 수정할 수 없는 상황이거나, OpenAPI를 사용하는 경우에는 클라이언트에서 처리를 해줘야 한다. 이를 위해 개발 환경 뿐만이 아닌 테스트, 배포 환경에서도 사용할 수 있는 프록시 서버를 적용할 수 있고, 다음과 같은 장점이 있다.
- 보안 강화 : 클라이언트 코드에서 서버의 주소를 노출하지 않고 요청할 수 있다.
- 재요청 시 응답 속도 증가 : 브라우저가 보낸 요청들을 프록시 서버에 캐시하여 재요청 시 서버를 거치지 않고 데이터를 주고 받을 수 있다.
이를 위해 현재 나와 있는 무료 프록시 서버 대여 서비스를 이용하거나, 프록시 서버를 직접 구축하는 방법 등이 존재한다.
1. 무료 프록시 대여 서비스 이용
GitHub - raravel/cors-proxy: Bypass CORS blocking api.
Bypass CORS blocking api. Contribute to raravel/cors-proxy development by creating an account on GitHub.
github.com
<script src='https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js'></script>
<script>
axios({
url: 'https://cors-proxy.org/api/',
method: 'get',
headers: {
'cors-proxy-url' : 'https://google.com/' //요청을 보낼 서버의 url
},
}).then((res) => {
console.log(res.data);
})
</script>
2. 프록시 서버 구축
- nginx를 사용하여 프록시 서버를 만드는 방법 : https://velog.io/@jeff0720/2018-11-18-2111-%EC%9E%91%EC%84%B1%EB%90%A8-iojomvsf0n
Nginx를 사용하여 프록시 서버 만들기
서론 안녕하세요, 영훈입니다. 회사에 입사한 후 인프라에 중요성에 대해서 깨닳게된 후 꾸준히 공부하고 있습니다. 오늘은 제가 공부한 Nginx의 개념에 대해서 간단히 정리한 후 Nginx 설치 및 사
velog.io
- netlify.toml을 사용하여 서버를 세팅하는 방법 : https://docs.netlify.com/routing/redirects/rewrites-proxies/#proxy-to-another-service
Rewrites and proxies
Use rewrite rules to fetch a location behind the scenes while the URL in the visitor’s address bar remains the same. Proxy to another service or site.
docs.netlify.com
1. [MDN] CORS 문서 : https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
교차 출처 리소스 공유 (CORS) - HTTP | MDN
교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라
developer.mozilla.org
2. [개발자 난기수의 공간] 교차 출처 리소스 공유 (CORS) : 개발자라면 한번은 보았던 것 : https://nankisu.tistory.com/70
교차 출처 리소스 공유 (CORS) : 개발자라면 한번은 보았던 것
동일 출처 정책(Same-Origin Policy) 사이드 프로젝트를 진행하다보면 자주 보는 것이 하나있는데 바로 CORS 에러다. CORS 에러는 브라우저의 동일 출처 정책(Same-Origin Policy)을 위반하면 발생하는 에러인
nankisu.tistory.com
3. [Inpa Dev] 악명 높은 CORS 개념 & 해결법 - 정리 끝판왕 : https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F#%EC%9A%94%EC%B2%AD_%EB%B0%A9%EC%8B%9D%EC%97%90_%EB%94%B0%EB%9D%BC_%EB%8B%A4%EB%A5%B8_cors_%EB%B0%9C%EC%83%9D_%EC%97%AC%EB%B6%80
🌐 악명 높은 CORS 개념 & 해결법 - 정리 끝판왕 👏
악명 높은 CORS 에러 메세지 웹 개발을 하다보면 반드시 마주치는 멍멍 같은 에러가 바로 CORS 이다. 웹 개발의 신입 신고식이라고 할 정도로, CORS는 누구나 한 번 정도는 겪게 된다고 해도 과언이
inpa.tistory.com
'스터디' 카테고리의 다른 글
인터페이스와 추상클래스 사용법 : 디자인패턴 맛보기 (0) | 2024.07.08 |
---|