HTTP 프로토콜에서 HTTPS 프로토콜로 변경하면서 예상치못한 문제들 덕분에 2~3일 정도 씨름하며 인프라 아키텍쳐에 대한 깊은 고민과 큰 깨달음 얻을 수 있었다.
주제는 크게 Mixed Content 와 CORS Policy 다.
첫번째는 기존에 브라우저가 백엔드 서버에 HTTP 요청을 보내며 통신하던 것이, HTTPS 페이지로 변환되면서 Mixed Content 문제를 야기했고
두번째는 Application Load Balancer 가 포함된, 즉 프록시 서버가 하나 붙은 상황에서 CORS 대응을 고민하다보니 프록시 서버의 X-Forwarded 헤더와 CORS 관련된 Access-Control-Allow-Origin 헤더에 대한 정확한 깨달음을 얻게 되었다.
목차
1. Mixed Content 란?
2. 프록시 서버는 CORS와 관계가 없다
3. 프록시 서버에 CORS 설정을 해줄 수도 있다
4. 아키텍쳐 개선해야됨
현재 25.2.11 시점에서 인프라 아키텍쳐는 위와 같다.
기존에는 EC2에 HTTP 프로토콜로 바로 접속하였지만 HTTPS 설정이 들어가면서 로드밸런서가 붙게된 구조다
(ACM은 EC2에 설치못하고 ALB에 설치해야하기 때문에 위와같은 애매한 아키텍쳐가 됨. 어떻게 개선할지는 글 마지막에에서 생각해보았다)
(1) HTTPS 요청이 들어오면 8080 포트번호일 경우엔 HTTP 프로토콜로 변환해서 EC2 인스턴스 8080 포트로 접속시키고 8080:8080 포트포워딩을 통해 톰캣 WAS에 접속
(2) HTTPS 기본 443 포트번호일 경우에는 HTTTP 프로토콜로 변환해 EC2 인스턴스 3000 포트로 접속시키고 3000:80 포트포워딩을 통해 Nginx 웹서버에 접속
이제 유저는 브라우저를 통해 https://doamain.com 으로 접속해서 Nginx 가 서빙한 화면을 보게될 것이고 브라우저에서는 API 요청을 통해 동적인 데이터를 받아온다.
이전에는 http://<EC2 도메인>:8080 으로 백엔드 서버에 API 요청을 하고 있었으므로 Mixed Content 문제가 발생한다.
1. Mixed Content 란?
웹 페이지에서 보안 연결(HTTPS)을 통해 제공되는 페이지에 비해 보안되지 않은 연결(HTTP)을 통해 로드되는 콘텐츠가 있는 경우 발생한다. 일반적으로 HTTPS로 보호된 페이지에 HTTP로 로드되는 이미지, 스크립트 또는 스타일 시트와 같은 외부 리소스가 있는 경우에 발생한다.
쉽게말해 요청은 HTTPS 로 보내는데 응답은 HTTP로 오는 경우에 발생한다.
- 브라우저는 HTTPS에서 HTTP로 내려가는 요청을 '보안 위반'으로 간주한다.
- 특히, JS에서 Fetch API로 직접 API 호출할 때 문제가 발생
- Mixed Content 문제 때문에 CORS를 설정해도 브라우저에서 차단될 수 있음
👉해결책은 그냥 https 로 보내면된다.
👉 만약 꾸역꾸역 HTTP로 요청을 보내고 싶다면 헤더에 X-Forwarded-Proto 를 활용해서 HTTPS에서 받은 요청임을 백엔드가 인식하도록 설정하면 될 것. X-Fowraded-* 헤더는 서버 간의 요청이 여러 프록시 서버를 거칠 때, 클라이언트의 원래 정보를 전달하는 데 사용되는 HTTP 헤더로 Proto 에 https 를 써주면 백엔드가 '아 이거 원래 https 요청이구나' 라고 인식한다.
2. 프록시 서버는 CORS와 관계가 없다
원래는 ALB에 443으로 프론트 서버에만 접근하다가, mixed content 문제를 해결하기 위해 ALB에 8080리스너를 추가했고 https://domain.com:8080 으로 서버에 https 요청을 보내게 되었다.
(즉 브라우저에선 https://domain.com 으로 접속해 화면을 보고 이 곳에서 https://domain.com:8080 으로 API 요청)
이때 CORS 문제가 발생했는데 결론만 말하면 스킴이 바뀌면서 다른 Origin 이 되었기 때문이다.
CORS 몰랐다면 클릭
리액트 개발하다보면 필연적으로 마주치는 CORS 문제.확실하게 정리해보기 목차1. 아무 사이트에서나 요청을 받아주면 무슨일이 생길까?2. CORS란?3. CSRF란?4. 스프링 시큐리티에서는 CSRF를 비활
biz-ninza.tistory.com
(CORS 에 대해 이해가 부족하다면 제가 작성한 위의 글을 읽으시면 바로 이해가 될 것입니다!)
Origin 이 달라짐을 인식하였으나 나는 여기서 큰 착각에 빠진다.
CORS 문제가 발생한 원인이, ALB 를 거치면서 로드밸런서의 IP를 Origin 으로 해석되기 때문이라고 생각했고 스프링 서버에 로드밸런서 IP를 적어야된다고 생각했다.
이는 CORS 에 대해 무지했기 때문에 빠진 함정이다.
1~4번과정에서 서버의 HTTP 메세지를 관찰해보자
GET /api/data HTTP/1.1
Host: www.idealstudy.store
Origin: https://www.idealstudy.store // 브라우저의 origin
User-Agent: Mozilla/5.0 ...
Accept: application/json
① 먼저, 브라우저가 로드밸런서에 요청을 보낼때에 origin 을 기록한다.
(Host 헤더는 도달해야할 목적지 서버를 지정한다. 특히 동일 IP 내에 여러 개의 도메인을 돌리는 가상 호스팅에서 중요한 개념)
GET /api/data HTTP/1.1
Host: www.idealstudy.store
Origin: https://www.idealstudy.store
User-Agent: Mozilla/5.0 ...
Accept: application/json
X-Forwarded-For: 172.31.XX.XX // 로드밸런서의 IP
X-Forwarded-Proto: https // 로드밸런서가 받은 요청의 프로토콜
X-Forwarded-Port: 443 // 로드밸런서가 받은 요청의 포트
X-Forwarded-Host: www.idealstudy.store // 로드밸런서가 받은 요청의 원래 호스트이름
② 로드밸런서는 여기에 자신의 IP와 받은 요청의 프로토콜을 기록한다. 단 이 때 origin 은 바뀌지 않는다!!
(거치는 프록시서버가 여러 개라면 해당 프록시서버의 IP가 X-Forwarded-For 에 순차적으로 기록된다)
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://www.idealstudy.store
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type
③ 백엔드 서버는 'Access-Control-Allow-Origin' 에 미리 준비된 헤더를 찍는다.
(즉 스프링 시큐리티 configuration 에서 allowed Origin 처리한 것은 응답시 이 헤더를 찍겠다는 의미)
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://www.idealstudy.store
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type
④ 최종적으로 로드밸런서는 위와같은 메세지를 전달하며 브라우저는 Access-Control-Allow-Origin 을 보며 본인과 같은 출처임을 인식해 CORS 블로킹을 하지 않는다.
=> 즉 LB는 origin 과 아무 관련이 없다. 리버스 프록싱은 단순히 트레픽을 전달할 뿐이지 origin 을 변경시키지 않는다. 다만 여기에 X-Forwarded-* 헤더로 이것이 본래 어떤 요청이었는지 그리고 자신이 누군이지 기록을 남길 뿐이다.
=> 고로 스프링 서버에는 브라우저의 url 만 찍으면 된다.
3. 프록시 서버에 CORS 설정을 해줄 수도 있다
결론적으로 CORS 를 해결하려면 브라우저가 받을 때 Access-Control-Allow-Origin 헤더만 본인 url 에 맞게만 잘 찍혀있으면 된다.
이를 다시말하면 백엔드 서버가 아니라 중간에 프록시서버가 브라우저에 돌려줄때 임의로 헤더만 찍어주면 CORS 를 탈출할 수 있다는 얘기다
location / {
# 정적 파일을 서빙하거나, 백엔드 서버로 프록시 요청을 보낼 때
proxy_pass http://your-backend-server;
# CORS 설정
add_header 'Access-Control-Allow-Origin' 'https://www.idealstudy.store' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
}
가령 브라우저 요청시 Nginx 를 거쳐 백엔드로 들어가는 구조라면 프록시 역할을 하는 Nginx 에 위와같이 응답에 대한 헤더에 출처를 적어주게 되면 브라우저는 아무문제없이 CORS를 통과시킨다
(아마 ALB 에도 비슷하게 헤더를 찍어줄 수 있는 방법이 있지 않을까 싶다)
만약 본인이 프론트이고 서버가 외부업체라 그저 빌릴뿐이고 협업이 안된다면 어쩔 수 없이 프론트에서 해결해야할 것이다. 그럴 경우 프록시 서버를 사용해 우회하거나, 직접 프록시 서버를 구축하면 된다. 프론트와 외부업체 사이에 프록시 서버를 두고, 서버가 프록시 서버로 응답을 해주면 프록시 서버는 응답에다가 Access-Control-Allow-Origin 헤더를 추가해서 프론트로 반환하면 된다. 이런 프록시서버를 통해 우회하는 방법을 간편하기위해 Webpack dev server 이용해서 해결한다고한다. ㅡ said https://jinhoon227.github.io/posts/%EC%8A%A4%ED%94%84%EB%A7%81CORS/
4. 아키텍쳐 개선해야됨
지금 아키텍쳐에서 미흡한 점이 몇 가지가 있다.
백엔드와 프론트엔드를 분리운영하는데 포트기반이라 직관적이지 않고, 둘이 end point 는 분리시켜놓고 막상 EC2 에는 백엔드 프론트엔드 서버가 다 있어서 아키텍쳐가 좀 이상하다 (^^;;)
1. Nginx 를 리버스 프록시 역할을 시키며 경로기반 라우팅하도록 개선할 수 있음
- Nginx 를 앞단에 두고 가령 https://domain.com/api/ 일경우엔 Nginx 가 스프링 서버:8080 으로 라우팅하고, https://domain.com 일 경우엔 Nginx 가 리액트 서버:3000 으로 라우팅하도록 경로 기반 라우팅이 가능.
이렇게하면 톰캣 앞에선 Nginx 가 리버스 프록시 역할도 톡톡히하고 또 백엔드나 프론트 서버 같은 포트번호를 쓰니까 CORS 문제도 덜생긴다
2. 도메인기반 라우팅하며 백엔드와 프론트엔드 서버를 완전 분리 가능
- 현재 EC2내에 백엔드 프론트서버가 동시에 있고 이것을 포트기반으로 분리시켰는데, https://api.domain.com 의 경우엔 스프링서버로 https://www.domain.com 의 경우엔 리액트 서버로 도메인별 라우팅을하면서 백엔드와 프론트를 직관적으로 완전히 분리가 가능하겠다.
이렇게하면 DNS 서버관리나 CORS 는 좀 신경을 쓰긴 해도 API 서버를 Lambda 나 ECS 같은 다른 인프라로 확장 가능하고 API 서버만 보안을 강화하거나 스케일링을 하는 등의 장점이 있겠다.
(백엔드와 프론트를 같은 서버에 두면 커플링이 생겨서 다른 편의 부하에 영향을 받거나 배포나 스케일링도 따로 할 수가 없다. 또 같은 보안그룹을 공유하므로 세밀한 보안관리가 어렵다)
3. HTTPS 를 인스턴스까지 유지할 수 있음
- AWS 인증서를 쓰려면 ALB를 사용해야하고 Nginx 까지 HTTPS 요청을 이어지게 하려면 Let's Encrypt 등의 인증서를 추가 발급받아 Nginx 에 설정해야하는 번거로움이 있어서 일단 브라우저와 ALB에만 HTTPS 통신을 하게 했는데 추후 보안을 더 강화하며 HTTPS 를 끝까지 유지할 수 있겠음
ㅡ 향후 공부 키워드
Preflight Request
SpringSecurity 와 SpringMVC 에서의 CORS
'[개발일지] > CS 스터디 플랫폼' 카테고리의 다른 글
11장. 상태관리 라이브러리 Context API 말고 다른거 쓰자. Redux? Zustand? (0) | 2025.02.28 |
---|---|
10장. 운영환경에서 시크릿 파일 관리 노하우 (0) | 2025.02.11 |
8장. EC2 .pem 파일을 분실했을 때 해결방법을 생각해보자 (a.k.a /home/ubuntu 폴더를 삭제해버렸다) (0) | 2025.02.04 |
7장. 도메인에 HTTPS 설정해야지. 인증서는 어디서? (0) | 2025.01.30 |
6장. 도메인 만들어야지. Gabia와 Route53 중에 뭘 쓸까? (0) | 2025.01.23 |