이전 장(링크) 에서는 CORS
란 무엇인지에 대해서 알아보았다.
이번 장에서는 실제로 현업에서 만날수 있는 상황을 예로 들어 해결하는 방법에 대해서 알아본다.
개발 환경
- OS: CentOS: 7
- Docker: 20.10.17
- Docker-compose: 1.27.4
- Spring Boot: 2.7.0
- Nginx: 1.21.6
상황
필자가 자주 경험한 두 상황을 예로 들어 해결하는 방법을 알아보려 한다.
상황 1
개발자가 자신의 PC에서 프론트엔드 코드를 직접 빌드하여 서버에 있는 API 서버에게 요청을 보내는 상황이다.
개발자 PC의 브라우저에
http://localhost:3000
으로 접속한다.Origin
은http://localhost:3000
이 된다.브라우저의 요청을 받은
Reverse Proxy
인Nginx
는 요청을API 서버
로 전달한다.API 서버
는Nginx
로 요청을 반환한다.응답을 받은 브라우저는
Origin
과 응답한 서버의 출처인api.your-domain.com
는 서로 다르기 때문에SOP
(Same-origin policy)를 위반했다고 판단하여CORS
에러를 출력하며 응답에 대한 결과물을 화면에 표시하지 않는다.
상황 2
도커 서버 내부에 리버스프록시
, 프론트엔드
, API 서버
가 실행되고 있으며 고객이 PC의 브라우저를 통해 프론트엔드 서비스에 접속하는 상황이다.
고객은
https://your-domain.com
이라는 주소를 입력하고 서버에 요청을 보낸다.
이때Origin
은https://your-domain.com
이 된다.요청을 받은
Nginx
는 브라우저에게 화면을 렌더링 할 수 있도록 프론트엔드 코드를 응답한다.이후에
API
요청이 필요한 기능을 고객이 클릭하면API 서버
로 요청을 보내게 된다.요청은
Nginx
를 거쳐서API 서버
에 전달되고API 서버
에 의해 처리된 결과를Nginx
는 고객의 브라우저에게 전달한다.Origin
은https://your-domain.com
이고 요청을 처리한API 서버
의 출처는https://api.your-domain.com
으로 출처가 다르기 때문에 브라우저는SOP
를 위반했다고 판단하여CORS
에러를 출력하며 응답에 대한 결과물을 화면에 표시하지 않는다.
해결방법
대부분의 서비스는 요청에 인증 정보가 포함되기 때문에 이번 예시에도 인증 정보가 헤더에 포함된다고 가정하고 해결해본다.
상황 1
수정하기 전의 Nginx
설정을 살펴본다.
upstream docker-api {
server api:8080;
}
server {
listen 80;
listen [::]:80;
server_name api.your-domain.net www.api.your-domain.net;
if ($http_x_forwarded_proto = 'http') {
return 301 https://$host$request_uri;
}
location / {
proxy_pass http://docker-api;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
api.your-domain.net
도메인으로 http
요청이 오는 경우 https
를 사용하여 443 포트로 리다이렉트 하고 있다.https
요청을 받게 되는 경우 API 서버
가 실행되고 있는 컨테이너로 요청을 전달하게 된다. server api:8080
에서 api
는 API 서버
가 실행되고 있는 컨테이너의 이름이다.
필자의 PC에서 프론트엔드 코드를 실행시켜 localhost:3000
주소로 접속하여 API 서버
로 요청을 보내본다.
에러 메시지를 확인해보면 Origin
인 http://localhost:3000
으로부터 http://api.your-domain.net
으로 요청을 보냈지만 헤더의 Access-Control-Allow-Origin
값이 포함되어 있지 않기 때문에 CORS policy 에 의해 block 되었다고 출력되고 있다.
문제를 해결하기 위해 아래와 같이 Nginx
설정을 수정해본다.
upstream docker-api {
server api:8080;
}
server {
listen 80;
listen [::]:80;
server_name api.your-domain.net www.api.your-domain.net;
if ($http_x_forwarded_proto = 'http') {
return 301 https://$host$request_uri;
}
if ($host = 'www.api.your-domain.net') {
return 301 https://api.your-domain.net$request_uri;
}
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
set $cors '';
if ($http_origin ~ 'http(s)?:\/\/(localhost(:\d{1,5}?)|your-domain\.net)$') {
set $cors 'true';
}
if ($cors = 'true') {
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With';
add_header 'Access-Control-Expose-Headers' 'Authorization';
}
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_set_header HOST $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_pass http://docker-api;
}
}
HTTP
로 요청이 오는 경우HTTPS
로 리다이렉트 되도록 하였다.www.api.your-domain.net
으로 요청이 오는 경우 통일성을 위해api.your-domain.net
으로 리다이렉트 되도록 하였다.- HTTP 메서드가
OPTIONS
인 경우 preflight 요청 및 응답을 할 수 있도록 헤더 값을 설정한다. Origin
이 우리가 정한 정규식을 통과한 경우 헤더에 허용 가능한Origin
,Credentials
,Methods
,Headers
값을 설정해준다.Nginx
를Reverse Proxy
로 사용하는 경우Nginx
에서 추가한 헤더가 반영되지 않는 경우가 있다.proxy_hide_header
를 사용해서 반드시 반영되어야 하는 헤더 값을 지정한다.
입력된 Origin
의 정보를 Access-Control-Allow-Origin
의 값으로 입력하거나 *
로 처리하는 방법이 훨씬 간단하게 해결된다.
하지만 상용 서비스에서 모든 Origin
을 허용하는 것은 보안상 좋지 않은 방식이다. 우리가 지정한 Origin
만 통과하도록 해야한다.
물론 http://localhost:3000
이라는 Origin
을 허용하는 것도 보안상 좋지 않은 방식이다. 부득이하게 로컬 환경에서 서버의 API를 요청해야 하는 경우 커스텀 헤더 값을 만들고 정해진 헤더 값을 전달하는 요청만 처리되도록 하는 것이 좋다.
설정을 변경하고 Nginx
를 재실행하고 다시 접속해보면 CORS
에러없이 정상적으로 API 요청의 결과가 화면에 표시되는 것을 확인할 수 있다.
상황 2
로컬 환경의 요청에서 발생하는 CORS
에러는 위의 방식대로 해결되었다.
설정을 살펴보면 https://your-domain.com
으로 들어오는 요청도 localhost
와 동일하게 정상 처리되어야 한다. 하지만 웹 페이지에 접속해보면 CORS
에러가 발생하는 것을 확인할 수 있다.
Nginx
에서 헤더를 추가했기 때문에 정상적으로 요청이 처리되었어야 한다. 하지만 CORS
에러가 발생하였고 정확한 원인은 파악하지 못했다.
하지만 스프링 부트 기준으로 해결방법은 발견하였다. 아래와 같이 CORS
관련 Bean
을 등록하여 WAS
에서 헤더에 값을 추가할 수 있도록 수정하는 방법이다.
(스프링 시큐리티를 사용하는 서비스는 아래와 같은 방식이 아니라 스프링 시큐리티를 사용하는 방식으로 적용해야 한다. 구글에 수많은 자료가 있으므로 본 문서에서는 다루지 않는다.)
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
private static final String[] ALLOWED_ORIGINS = {
"http://localhost:3000",
"https://your-domain.net"
};
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(ALLOWED_ORIGINS)
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
}
구성파일이 추가된 도커 이미지를 만들고 API 서버를 재실행시키면 정상적으로 CORS 에러 없이 API 요청이 화면에 표시되는 것을 확인할 수 있다.
CORS
란 무엇이지 알아보았던 이전 장에 이어 현업에서 마주칠 수 있는 CORS
에러를 직접 해결하는 방법에 대해서 알아보았다.
아직 상황 2에서 왜 추가로 스프링에 빈을 등록하여 헤더에 값을 추가해야 하는지에 대해서는 해결되지 않았다.
물론 같은 도커 서버 내부에서 private 통신을 하기 때문에 API 서버를 요청하는 시점에는 Nginx
를 거치지 않기 때문이라고 추측은 된다.
이번에는 시관관계상 글을 마무리하고 추후에 테스트를 진행해보고 결과를 공유하도록 하겠다.
참고한 문서
'Infrastructure > Network' 카테고리의 다른 글
[Network] CORS 란? (0) | 2022.06.20 |
---|---|
[HTTP] 헤더 - 4 (쿠키) (0) | 2021.06.28 |
[HTTP] 헤더 - 3 (일반 정보) (0) | 2021.06.28 |
[HTTP] 헤더 - 2 (전송 방식) (0) | 2021.06.28 |
[HTTP] 헤더 - 6 (조건부 요청) (0) | 2021.06.28 |